summaryrefslogtreecommitdiffstats
path: root/src/rocksdb/tools
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-21 11:54:28 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-21 11:54:28 +0000
commite6918187568dbd01842d8d1d2c808ce16a894239 (patch)
tree64f88b554b444a49f656b6c656111a145cbbaa28 /src/rocksdb/tools
parentInitial commit. (diff)
downloadceph-b26c4052f3542036551aa9dec9caa4226e456195.tar.xz
ceph-b26c4052f3542036551aa9dec9caa4226e456195.zip
Adding upstream version 18.2.2.upstream/18.2.2
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/rocksdb/tools')
-rw-r--r--src/rocksdb/tools/CMakeLists.txt30
-rw-r--r--src/rocksdb/tools/Dockerfile5
-rw-r--r--src/rocksdb/tools/advisor/README.md96
-rw-r--r--src/rocksdb/tools/advisor/advisor/__init__.py0
-rw-r--r--src/rocksdb/tools/advisor/advisor/bench_runner.py39
-rw-r--r--src/rocksdb/tools/advisor/advisor/config_optimizer_example.py140
-rw-r--r--src/rocksdb/tools/advisor/advisor/db_bench_runner.py237
-rw-r--r--src/rocksdb/tools/advisor/advisor/db_config_optimizer.py293
-rw-r--r--src/rocksdb/tools/advisor/advisor/db_log_parser.py134
-rw-r--r--src/rocksdb/tools/advisor/advisor/db_options_parser.py348
-rwxr-xr-xsrc/rocksdb/tools/advisor/advisor/db_stats_fetcher.py346
-rw-r--r--src/rocksdb/tools/advisor/advisor/db_timeseries_parser.py203
-rw-r--r--src/rocksdb/tools/advisor/advisor/ini_parser.py76
-rw-r--r--src/rocksdb/tools/advisor/advisor/rule_parser.py510
-rw-r--r--src/rocksdb/tools/advisor/advisor/rule_parser_example.py98
-rw-r--r--src/rocksdb/tools/advisor/advisor/rules.ini214
-rw-r--r--src/rocksdb/tools/advisor/test/__init__.py0
-rw-r--r--src/rocksdb/tools/advisor/test/input_files/LOG-030
-rw-r--r--src/rocksdb/tools/advisor/test/input_files/LOG-125
-rw-r--r--src/rocksdb/tools/advisor/test/input_files/OPTIONS-00000549
-rw-r--r--src/rocksdb/tools/advisor/test/input_files/log_stats_parser_keys_ts3
-rw-r--r--src/rocksdb/tools/advisor/test/input_files/rules_err1.ini56
-rw-r--r--src/rocksdb/tools/advisor/test/input_files/rules_err2.ini15
-rw-r--r--src/rocksdb/tools/advisor/test/input_files/rules_err3.ini15
-rw-r--r--src/rocksdb/tools/advisor/test/input_files/rules_err4.ini15
-rw-r--r--src/rocksdb/tools/advisor/test/input_files/test_rules.ini47
-rw-r--r--src/rocksdb/tools/advisor/test/input_files/triggered_rules.ini83
-rw-r--r--src/rocksdb/tools/advisor/test/test_db_bench_runner.py141
-rw-r--r--src/rocksdb/tools/advisor/test/test_db_log_parser.py96
-rw-r--r--src/rocksdb/tools/advisor/test/test_db_options_parser.py214
-rw-r--r--src/rocksdb/tools/advisor/test/test_db_stats_fetcher.py121
-rw-r--r--src/rocksdb/tools/advisor/test/test_rule_parser.py226
-rwxr-xr-xsrc/rocksdb/tools/analyze_txn_stress_test.sh77
-rwxr-xr-xsrc/rocksdb/tools/auto_sanity_test.sh93
-rwxr-xr-xsrc/rocksdb/tools/backup_db.sh15
-rwxr-xr-xsrc/rocksdb/tools/benchmark.sh1173
-rwxr-xr-xsrc/rocksdb/tools/benchmark_ci.py182
-rwxr-xr-xsrc/rocksdb/tools/benchmark_compare.sh342
-rwxr-xr-xsrc/rocksdb/tools/benchmark_leveldb.sh187
-rw-r--r--src/rocksdb/tools/blob_dump.cc111
-rw-r--r--src/rocksdb/tools/block_cache_analyzer/__init__.py2
-rw-r--r--src/rocksdb/tools/block_cache_analyzer/block_cache_pysim.py2000
-rw-r--r--src/rocksdb/tools/block_cache_analyzer/block_cache_pysim.sh156
-rw-r--r--src/rocksdb/tools/block_cache_analyzer/block_cache_pysim_test.py734
-rw-r--r--src/rocksdb/tools/block_cache_analyzer/block_cache_trace_analyzer.cc2316
-rw-r--r--src/rocksdb/tools/block_cache_analyzer/block_cache_trace_analyzer.h397
-rw-r--r--src/rocksdb/tools/block_cache_analyzer/block_cache_trace_analyzer_plot.py729
-rw-r--r--src/rocksdb/tools/block_cache_analyzer/block_cache_trace_analyzer_test.cc735
-rw-r--r--src/rocksdb/tools/block_cache_analyzer/block_cache_trace_analyzer_tool.cc25
-rwxr-xr-xsrc/rocksdb/tools/check_all_python.py22
-rwxr-xr-xsrc/rocksdb/tools/check_format_compatible.sh379
-rw-r--r--src/rocksdb/tools/db_bench.cc21
-rw-r--r--src/rocksdb/tools/db_bench_tool.cc8707
-rw-r--r--src/rocksdb/tools/db_bench_tool_test.cc334
-rw-r--r--src/rocksdb/tools/db_crashtest.py1016
-rw-r--r--src/rocksdb/tools/db_repl_stress.cc140
-rw-r--r--src/rocksdb/tools/db_sanity_test.cc300
-rwxr-xr-xsrc/rocksdb/tools/dbench_monitor102
-rw-r--r--src/rocksdb/tools/dump/db_dump_tool.cc260
-rw-r--r--src/rocksdb/tools/dump/rocksdb_dump.cc63
-rw-r--r--src/rocksdb/tools/dump/rocksdb_undump.cc62
-rwxr-xr-xsrc/rocksdb/tools/generate_random_db.sh31
-rwxr-xr-xsrc/rocksdb/tools/ingest_external_sst.sh18
-rw-r--r--src/rocksdb/tools/io_tracer_parser.cc25
-rw-r--r--src/rocksdb/tools/io_tracer_parser_test.cc190
-rw-r--r--src/rocksdb/tools/io_tracer_parser_tool.cc144
-rw-r--r--src/rocksdb/tools/io_tracer_parser_tool.h40
-rw-r--r--src/rocksdb/tools/ldb.cc21
-rw-r--r--src/rocksdb/tools/ldb_cmd.cc4263
-rw-r--r--src/rocksdb/tools/ldb_cmd_impl.h744
-rw-r--r--src/rocksdb/tools/ldb_cmd_test.cc1226
-rw-r--r--src/rocksdb/tools/ldb_test.py955
-rw-r--r--src/rocksdb/tools/ldb_tool.cc184
-rwxr-xr-xsrc/rocksdb/tools/pflag217
-rw-r--r--src/rocksdb/tools/reduce_levels_test.cc222
-rwxr-xr-xsrc/rocksdb/tools/regression_test.sh477
-rwxr-xr-xsrc/rocksdb/tools/restore_db.sh15
-rwxr-xr-xsrc/rocksdb/tools/rocksdb_dump_test.sh9
-rwxr-xr-xsrc/rocksdb/tools/run_blob_bench.sh223
-rwxr-xr-xsrc/rocksdb/tools/run_flash_bench.sh359
-rwxr-xr-xsrc/rocksdb/tools/run_leveldb.sh175
-rw-r--r--src/rocksdb/tools/sample-dump.dmpbin0 -> 100 bytes
-rw-r--r--src/rocksdb/tools/simulated_hybrid_file_system.cc245
-rw-r--r--src/rocksdb/tools/simulated_hybrid_file_system.h126
-rw-r--r--src/rocksdb/tools/sst_dump.cc20
-rw-r--r--src/rocksdb/tools/sst_dump_test.cc481
-rw-r--r--src/rocksdb/tools/sst_dump_tool.cc584
-rw-r--r--src/rocksdb/tools/trace_analyzer.cc25
-rw-r--r--src/rocksdb/tools/trace_analyzer_test.cc890
-rw-r--r--src/rocksdb/tools/trace_analyzer_tool.cc1925
-rw-r--r--src/rocksdb/tools/trace_analyzer_tool.h326
-rwxr-xr-xsrc/rocksdb/tools/verify_random_db.sh41
-rwxr-xr-xsrc/rocksdb/tools/write_external_sst.sh26
-rw-r--r--src/rocksdb/tools/write_stress.cc309
-rw-r--r--src/rocksdb/tools/write_stress_runner.py83
95 files changed, 39204 insertions, 0 deletions
diff --git a/src/rocksdb/tools/CMakeLists.txt b/src/rocksdb/tools/CMakeLists.txt
new file mode 100644
index 000000000..19030e84b
--- /dev/null
+++ b/src/rocksdb/tools/CMakeLists.txt
@@ -0,0 +1,30 @@
+set(CORE_TOOLS
+ sst_dump.cc
+ ldb.cc)
+foreach(src ${CORE_TOOLS})
+ get_filename_component(exename ${src} NAME_WE)
+ add_executable(${exename}${ARTIFACT_SUFFIX}
+ ${src})
+ target_link_libraries(${exename}${ARTIFACT_SUFFIX} ${ROCKSDB_LIB})
+ list(APPEND core_tool_deps ${exename})
+endforeach()
+
+if(WITH_TOOLS)
+ set(TOOLS
+ db_sanity_test.cc
+ write_stress.cc
+ db_repl_stress.cc
+ dump/rocksdb_dump.cc
+ dump/rocksdb_undump.cc)
+ foreach(src ${TOOLS})
+ get_filename_component(exename ${src} NAME_WE)
+ add_executable(${exename}${ARTIFACT_SUFFIX}
+ ${src})
+ target_link_libraries(${exename}${ARTIFACT_SUFFIX} ${ROCKSDB_LIB} ${THIRDPARTY_LIBS})
+ list(APPEND tool_deps ${exename})
+ endforeach()
+
+ add_custom_target(ldb_tests
+ COMMAND python ${CMAKE_CURRENT_SOURCE_DIR}/ldb_tests.py
+ DEPENDS ldb)
+endif()
diff --git a/src/rocksdb/tools/Dockerfile b/src/rocksdb/tools/Dockerfile
new file mode 100644
index 000000000..1d5ead7fd
--- /dev/null
+++ b/src/rocksdb/tools/Dockerfile
@@ -0,0 +1,5 @@
+FROM buildpack-deps:wheezy
+
+ADD ./ldb /rocksdb/tools/ldb
+
+CMD /rocksdb/tools/ldb
diff --git a/src/rocksdb/tools/advisor/README.md b/src/rocksdb/tools/advisor/README.md
new file mode 100644
index 000000000..b02d7ec50
--- /dev/null
+++ b/src/rocksdb/tools/advisor/README.md
@@ -0,0 +1,96 @@
+# Rocksdb Tuning Advisor
+
+## Motivation
+
+The performance of Rocksdb is contingent on its tuning. However,
+because of the complexity of its underlying technology and a large number of
+configurable parameters, a good configuration is sometimes hard to obtain. The aim of
+the python command-line tool, Rocksdb Advisor, is to automate the process of
+suggesting improvements in the configuration based on advice from Rocksdb
+experts.
+
+## Overview
+
+Experts share their wisdom as rules comprising of conditions and suggestions in the INI format (refer
+[rules.ini](https://github.com/facebook/rocksdb/blob/main/tools/advisor/advisor/rules.ini)).
+Users provide the Rocksdb configuration that they want to improve upon (as the
+familiar Rocksdb OPTIONS file —
+[example](https://github.com/facebook/rocksdb/blob/main/examples/rocksdb_option_file_example.ini))
+and the path of the file which contains Rocksdb logs and statistics.
+The [Advisor](https://github.com/facebook/rocksdb/blob/main/tools/advisor/advisor/rule_parser_example.py)
+creates appropriate DataSource objects (for Rocksdb
+[logs](https://github.com/facebook/rocksdb/blob/main/tools/advisor/advisor/db_log_parser.py),
+[options](https://github.com/facebook/rocksdb/blob/main/tools/advisor/advisor/db_options_parser.py),
+[statistics](https://github.com/facebook/rocksdb/blob/main/tools/advisor/advisor/db_stats_fetcher.py) etc.)
+and provides them to the [Rules Engine](https://github.com/facebook/rocksdb/blob/main/tools/advisor/advisor/rule_parser.py).
+The Rules uses rules from experts to parse data-sources and trigger appropriate rules.
+The Advisor's output gives information about which rules were triggered,
+why they were triggered and what each of them suggests. Each suggestion
+provided by a triggered rule advises some action on a Rocksdb
+configuration option, for example, increase CFOptions.write_buffer_size,
+set bloom_bits to 2 etc.
+
+## Usage
+
+### Prerequisites
+The tool needs the following to run:
+* python3
+
+### Running the tool
+An example command to run the tool:
+
+```shell
+cd rocksdb/tools/advisor
+python3 -m advisor.rule_parser_example --rules_spec=advisor/rules.ini --rocksdb_options=test/input_files/OPTIONS-000005 --log_files_path_prefix=test/input_files/LOG-0 --stats_dump_period_sec=20
+```
+
+### Command-line arguments
+
+Most important amongst all the input that the Advisor needs, are the rules
+spec and starting Rocksdb configuration. The configuration is provided as the
+familiar Rocksdb Options file (refer [example](https://github.com/facebook/rocksdb/blob/main/examples/rocksdb_option_file_example.ini)).
+The Rules spec is written in the INI format (more details in
+[rules.ini](https://github.com/facebook/rocksdb/blob/main/tools/advisor/advisor/rules.ini)).
+
+In brief, a Rule is made of conditions and is triggered when all its
+constituent conditions are triggered. When triggered, a Rule suggests changes
+(increase/decrease/set to a suggested value) to certain Rocksdb options that
+aim to improve Rocksdb performance. Every Condition has a 'source' i.e.
+the data source that would be checked for triggering that condition.
+For example, a log Condition (with 'source=LOG') is triggered if a particular
+'regex' is found in the Rocksdb LOG files. As of now the Rules Engine
+supports 3 types of Conditions (and consequently data-sources):
+LOG, OPTIONS, TIME_SERIES. The TIME_SERIES data can be sourced from the
+Rocksdb [statistics](https://github.com/facebook/rocksdb/blob/main/include/rocksdb/statistics.h)
+or [perf context](https://github.com/facebook/rocksdb/blob/main/include/rocksdb/perf_context.h).
+
+For more information about the remaining command-line arguments, run:
+
+```shell
+cd rocksdb/tools/advisor
+python3 -m advisor.rule_parser_example --help
+```
+
+### Sample output
+
+Here, a Rocksdb log-based rule has been triggered:
+
+```shell
+Rule: stall-too-many-memtables
+LogCondition: stall-too-many-memtables regex: Stopping writes because we have \d+ immutable memtables \(waiting for flush\), max_write_buffer_number is set to \d+
+Suggestion: inc-bg-flush option : DBOptions.max_background_flushes action : increase suggested_values : ['2']
+Suggestion: inc-write-buffer option : CFOptions.max_write_buffer_number action : increase
+scope: col_fam:
+{'default'}
+```
+
+## Running the tests
+
+Tests for the code have been added to the
+[test/](https://github.com/facebook/rocksdb/tree/main/tools/advisor/test)
+directory. For example, to run the unit tests for db_log_parser.py:
+
+```shell
+cd rocksdb/tools/advisor
+python3 -m unittest -v test.test_db_log_parser
+```
diff --git a/src/rocksdb/tools/advisor/advisor/__init__.py b/src/rocksdb/tools/advisor/advisor/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/rocksdb/tools/advisor/advisor/__init__.py
diff --git a/src/rocksdb/tools/advisor/advisor/bench_runner.py b/src/rocksdb/tools/advisor/advisor/bench_runner.py
new file mode 100644
index 000000000..45d6c8313
--- /dev/null
+++ b/src/rocksdb/tools/advisor/advisor/bench_runner.py
@@ -0,0 +1,39 @@
+# Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+# This source code is licensed under both the GPLv2 (found in the
+# COPYING file in the root directory) and Apache 2.0 License
+# (found in the LICENSE.Apache file in the root directory).
+
+import re
+from abc import ABC, abstractmethod
+
+
+class BenchmarkRunner(ABC):
+ @staticmethod
+ @abstractmethod
+ def is_metric_better(new_metric, old_metric):
+ pass
+
+ @abstractmethod
+ def run_experiment(self):
+ # should return a list of DataSource objects
+ pass
+
+ @staticmethod
+ def get_info_log_file_name(log_dir, db_path):
+ # Example: DB Path = /dev/shm and OPTIONS file has option
+ # db_log_dir=/tmp/rocks/, then the name of the log file will be
+ # 'dev_shm_LOG' and its location will be /tmp/rocks. If db_log_dir is
+ # not specified in the OPTIONS file, then the location of the log file
+ # will be /dev/shm and the name of the file will be 'LOG'
+ file_name = ""
+ if log_dir:
+ # refer GetInfoLogPrefix() in rocksdb/util/filename.cc
+ # example db_path: /dev/shm/dbbench
+ file_name = db_path[1:] # to ignore the leading '/' character
+ to_be_replaced = re.compile("[^0-9a-zA-Z\-_\.]") # noqa
+ for character in to_be_replaced.findall(db_path):
+ file_name = file_name.replace(character, "_")
+ if not file_name.endswith("_"):
+ file_name += "_"
+ file_name += "LOG"
+ return file_name
diff --git a/src/rocksdb/tools/advisor/advisor/config_optimizer_example.py b/src/rocksdb/tools/advisor/advisor/config_optimizer_example.py
new file mode 100644
index 000000000..40e2bb953
--- /dev/null
+++ b/src/rocksdb/tools/advisor/advisor/config_optimizer_example.py
@@ -0,0 +1,140 @@
+# Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+# This source code is licensed under both the GPLv2 (found in the
+# COPYING file in the root directory) and Apache 2.0 License
+# (found in the LICENSE.Apache file in the root directory).
+
+import argparse
+
+from advisor.db_config_optimizer import ConfigOptimizer
+from advisor.db_log_parser import NO_COL_FAMILY
+from advisor.db_options_parser import DatabaseOptions
+from advisor.rule_parser import RulesSpec
+
+
+CONFIG_OPT_NUM_ITER = 10
+
+
+def main(args):
+ # initialise the RulesSpec parser
+ rule_spec_parser = RulesSpec(args.rules_spec)
+ # initialise the benchmark runner
+ bench_runner_module = __import__(
+ args.benchrunner_module, fromlist=[args.benchrunner_class]
+ )
+ bench_runner_class = getattr(bench_runner_module, args.benchrunner_class)
+ ods_args = {}
+ if args.ods_client and args.ods_entity:
+ ods_args["client_script"] = args.ods_client
+ ods_args["entity"] = args.ods_entity
+ if args.ods_key_prefix:
+ ods_args["key_prefix"] = args.ods_key_prefix
+ db_bench_runner = bench_runner_class(args.benchrunner_pos_args, ods_args)
+ # initialise the database configuration
+ db_options = DatabaseOptions(args.rocksdb_options, args.misc_options)
+ # set the frequency at which stats are dumped in the LOG file and the
+ # location of the LOG file.
+ db_log_dump_settings = {
+ "DBOptions.stats_dump_period_sec": {NO_COL_FAMILY: args.stats_dump_period_sec}
+ }
+ db_options.update_options(db_log_dump_settings)
+ # initialise the configuration optimizer
+ config_optimizer = ConfigOptimizer(
+ db_bench_runner, db_options, rule_spec_parser, args.base_db_path
+ )
+ # run the optimiser to improve the database configuration for given
+ # benchmarks, with the help of expert-specified rules
+ final_db_options = config_optimizer.run()
+ # generate the final rocksdb options file
+ print(
+ "Final configuration in: " + final_db_options.generate_options_config("final")
+ )
+ print("Final miscellaneous options: " + repr(final_db_options.get_misc_options()))
+
+
+if __name__ == "__main__":
+ """
+ An example run of this tool from the command-line would look like:
+ python3 -m advisor.config_optimizer_example
+ --base_db_path=/tmp/rocksdbtest-155919/dbbench
+ --rocksdb_options=temp/OPTIONS_boot.tmp --misc_options bloom_bits=2
+ --rules_spec=advisor/rules.ini --stats_dump_period_sec=20
+ --benchrunner_module=advisor.db_bench_runner
+ --benchrunner_class=DBBenchRunner --benchrunner_pos_args ./../../db_bench
+ readwhilewriting use_existing_db=true duration=90
+ """
+ parser = argparse.ArgumentParser(
+ description="This script is used for\
+ searching for a better database configuration"
+ )
+ parser.add_argument(
+ "--rocksdb_options",
+ required=True,
+ type=str,
+ help="path of the starting Rocksdb OPTIONS file",
+ )
+ # these are options that are column-family agnostic and are not yet
+ # supported by the Rocksdb Options file: eg. bloom_bits=2
+ parser.add_argument(
+ "--misc_options",
+ nargs="*",
+ help="whitespace-separated list of options that are not supported "
+ + "by the Rocksdb OPTIONS file, given in the "
+ + '<option_name>=<option_value> format eg. "bloom_bits=2 '
+ + 'rate_limiter_bytes_per_sec=128000000"',
+ )
+ parser.add_argument(
+ "--base_db_path", required=True, type=str, help="path for the Rocksdb database"
+ )
+ parser.add_argument(
+ "--rules_spec",
+ required=True,
+ type=str,
+ help="path of the file containing the expert-specified Rules",
+ )
+ parser.add_argument(
+ "--stats_dump_period_sec",
+ required=True,
+ type=int,
+ help="the frequency (in seconds) at which STATISTICS are printed to "
+ + "the Rocksdb LOG file",
+ )
+ # ODS arguments
+ parser.add_argument("--ods_client", type=str, help="the ODS client binary")
+ parser.add_argument(
+ "--ods_entity",
+ type=str,
+ help="the servers for which the ODS stats need to be fetched",
+ )
+ parser.add_argument(
+ "--ods_key_prefix",
+ type=str,
+ help="the prefix that needs to be attached to the keys of time "
+ + "series to be fetched from ODS",
+ )
+ # benchrunner_module example: advisor.db_benchmark_client
+ parser.add_argument(
+ "--benchrunner_module",
+ required=True,
+ type=str,
+ help="the module containing the BenchmarkRunner class to be used by "
+ + "the Optimizer, example: advisor.db_bench_runner",
+ )
+ # benchrunner_class example: DBBenchRunner
+ parser.add_argument(
+ "--benchrunner_class",
+ required=True,
+ type=str,
+ help="the name of the BenchmarkRunner class to be used by the "
+ + "Optimizer, should be present in the module provided in the "
+ + "benchrunner_module argument, example: DBBenchRunner",
+ )
+ parser.add_argument(
+ "--benchrunner_pos_args",
+ nargs="*",
+ help="whitespace-separated positional arguments that are passed on "
+ + "to the constructor of the BenchmarkRunner class provided in the "
+ + 'benchrunner_class argument, example: "use_existing_db=true '
+ + 'duration=900"',
+ )
+ args = parser.parse_args()
+ main(args)
diff --git a/src/rocksdb/tools/advisor/advisor/db_bench_runner.py b/src/rocksdb/tools/advisor/advisor/db_bench_runner.py
new file mode 100644
index 000000000..f5802ed15
--- /dev/null
+++ b/src/rocksdb/tools/advisor/advisor/db_bench_runner.py
@@ -0,0 +1,237 @@
+# Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+# This source code is licensed under both the GPLv2 (found in the
+# COPYING file in the root directory) and Apache 2.0 License
+# (found in the LICENSE.Apache file in the root directory).
+
+import shutil
+import subprocess
+import time
+
+from advisor.bench_runner import BenchmarkRunner
+from advisor.db_log_parser import DatabaseLogs, DataSource, NO_COL_FAMILY
+from advisor.db_stats_fetcher import (
+ DatabasePerfContext,
+ LogStatsParser,
+ OdsStatsFetcher,
+)
+
+
+"""
+NOTE: This is not thread-safe, because the output file is simply overwritten.
+"""
+
+
+class DBBenchRunner(BenchmarkRunner):
+ OUTPUT_FILE = "temp/dbbench_out.tmp"
+ ERROR_FILE = "temp/dbbench_err.tmp"
+ DB_PATH = "DB path"
+ THROUGHPUT = "ops/sec"
+ PERF_CON = " PERF_CONTEXT:"
+
+ @staticmethod
+ def is_metric_better(new_metric, old_metric):
+ # for db_bench 'throughput' is the metric returned by run_experiment
+ return new_metric >= old_metric
+
+ @staticmethod
+ def get_opt_args_str(misc_options_dict):
+ # given a dictionary of options and their values, return a string
+ # that can be appended as command-line arguments
+ optional_args_str = ""
+ for option_name, option_value in misc_options_dict.items():
+ if option_value:
+ optional_args_str += " --" + option_name + "=" + str(option_value)
+ return optional_args_str
+
+ def __init__(self, positional_args, ods_args=None):
+ # parse positional_args list appropriately
+ self.db_bench_binary = positional_args[0]
+ self.benchmark = positional_args[1]
+ self.db_bench_args = None
+ if len(positional_args) > 2:
+ # options list with each option given as "<option>=<value>"
+ self.db_bench_args = positional_args[2:]
+ # save ods_args, if provided
+ self.ods_args = ods_args
+
+ def _parse_output(self, get_perf_context=False):
+ """
+ Sample db_bench output after running 'readwhilewriting' benchmark:
+ DB path: [/tmp/rocksdbtest-155919/dbbench]\n
+ readwhilewriting : 16.582 micros/op 60305 ops/sec; 4.2 MB/s (3433828\
+ of 5427999 found)\n
+ PERF_CONTEXT:\n
+ user_key_comparison_count = 500466712, block_cache_hit_count = ...\n
+ """
+ output = {self.THROUGHPUT: None, self.DB_PATH: None, self.PERF_CON: None}
+ perf_context_begins = False
+ with open(self.OUTPUT_FILE, "r") as fp:
+ for line in fp:
+ if line.startswith(self.benchmark):
+ # line from sample output:
+ # readwhilewriting : 16.582 micros/op 60305 ops/sec; \
+ # 4.2 MB/s (3433828 of 5427999 found)\n
+ print(line) # print output of the benchmark run
+ token_list = line.strip().split()
+ for ix, token in enumerate(token_list):
+ if token.startswith(self.THROUGHPUT):
+ # in above example, throughput = 60305 ops/sec
+ output[self.THROUGHPUT] = float(token_list[ix - 1])
+ break
+ elif get_perf_context and line.startswith(self.PERF_CON):
+ # the following lines in the output contain perf context
+ # statistics (refer example above)
+ perf_context_begins = True
+ elif get_perf_context and perf_context_begins:
+ # Sample perf_context output:
+ # user_key_comparison_count = 500, block_cache_hit_count =\
+ # 468, block_read_count = 580, block_read_byte = 445, ...
+ token_list = line.strip().split(",")
+ # token_list = ['user_key_comparison_count = 500',
+ # 'block_cache_hit_count = 468','block_read_count = 580'...
+ perf_context = {
+ tk.split("=")[0].strip(): tk.split("=")[1].strip()
+ for tk in token_list
+ if tk
+ }
+ # TODO(poojam23): this is a hack and should be replaced
+ # with the timestamp that db_bench will provide per printed
+ # perf_context
+ timestamp = int(time.time())
+ perf_context_ts = {}
+ for stat in perf_context.keys():
+ perf_context_ts[stat] = {timestamp: int(perf_context[stat])}
+ output[self.PERF_CON] = perf_context_ts
+ perf_context_begins = False
+ elif line.startswith(self.DB_PATH):
+ # line from sample output:
+ # DB path: [/tmp/rocksdbtest-155919/dbbench]\n
+ output[self.DB_PATH] = line.split("[")[1].split("]")[0]
+ return output
+
+ def get_log_options(self, db_options, db_path):
+ # get the location of the LOG file and the frequency at which stats are
+ # dumped in the LOG file
+ log_dir_path = None
+ stats_freq_sec = None
+ logs_file_prefix = None
+
+ # fetch frequency at which the stats are dumped in the Rocksdb logs
+ dump_period = "DBOptions.stats_dump_period_sec"
+ # fetch the directory, if specified, in which the Rocksdb logs are
+ # dumped, by default logs are dumped in same location as database
+ log_dir = "DBOptions.db_log_dir"
+ log_options = db_options.get_options([dump_period, log_dir])
+ if dump_period in log_options:
+ stats_freq_sec = int(log_options[dump_period][NO_COL_FAMILY])
+ if log_dir in log_options:
+ log_dir_path = log_options[log_dir][NO_COL_FAMILY]
+
+ log_file_name = DBBenchRunner.get_info_log_file_name(log_dir_path, db_path)
+
+ if not log_dir_path:
+ log_dir_path = db_path
+ if not log_dir_path.endswith("/"):
+ log_dir_path += "/"
+
+ logs_file_prefix = log_dir_path + log_file_name
+ return (logs_file_prefix, stats_freq_sec)
+
+ def _get_options_command_line_args_str(self, curr_options):
+ """
+ This method uses the provided Rocksdb OPTIONS to create a string of
+ command-line arguments for db_bench.
+ The --options_file argument is always given and the options that are
+ not supported by the OPTIONS file are given as separate arguments.
+ """
+ optional_args_str = DBBenchRunner.get_opt_args_str(
+ curr_options.get_misc_options()
+ )
+ # generate an options configuration file
+ options_file = curr_options.generate_options_config(nonce="12345")
+ optional_args_str += " --options_file=" + options_file
+ return optional_args_str
+
+ def _setup_db_before_experiment(self, curr_options, db_path):
+ # remove destination directory if it already exists
+ try:
+ shutil.rmtree(db_path, ignore_errors=True)
+ except OSError as e:
+ print("Error: rmdir " + e.filename + " " + e.strerror)
+ # setup database with a million keys using the fillrandom benchmark
+ command = "%s --benchmarks=fillrandom --db=%s --num=1000000" % (
+ self.db_bench_binary,
+ db_path,
+ )
+ args_str = self._get_options_command_line_args_str(curr_options)
+ command += args_str
+ self._run_command(command)
+
+ def _build_experiment_command(self, curr_options, db_path):
+ command = "%s --benchmarks=%s --statistics --perf_level=3 --db=%s" % (
+ self.db_bench_binary,
+ self.benchmark,
+ db_path,
+ )
+ # fetch the command-line arguments string for providing Rocksdb options
+ args_str = self._get_options_command_line_args_str(curr_options)
+ # handle the command-line args passed in the constructor, these
+ # arguments are specific to db_bench
+ for cmd_line_arg in self.db_bench_args:
+ args_str += " --" + cmd_line_arg
+ command += args_str
+ return command
+
+ def _run_command(self, command):
+ out_file = open(self.OUTPUT_FILE, "w+")
+ err_file = open(self.ERROR_FILE, "w+")
+ print("executing... - " + command)
+ subprocess.call(command, shell=True, stdout=out_file, stderr=err_file)
+ out_file.close()
+ err_file.close()
+
+ def run_experiment(self, db_options, db_path):
+ # setup the Rocksdb database before running experiment
+ self._setup_db_before_experiment(db_options, db_path)
+ # get the command to run the experiment
+ command = self._build_experiment_command(db_options, db_path)
+ experiment_start_time = int(time.time())
+ # run experiment
+ self._run_command(command)
+ experiment_end_time = int(time.time())
+ # parse the db_bench experiment output
+ parsed_output = self._parse_output(get_perf_context=True)
+
+ # get the log files path prefix and frequency at which Rocksdb stats
+ # are dumped in the logs
+ logs_file_prefix, stats_freq_sec = self.get_log_options(
+ db_options, parsed_output[self.DB_PATH]
+ )
+ # create the Rocksbd LOGS object
+ db_logs = DatabaseLogs(logs_file_prefix, db_options.get_column_families())
+ # Create the Log STATS object
+ db_log_stats = LogStatsParser(logs_file_prefix, stats_freq_sec)
+ # Create the PerfContext STATS object
+ db_perf_context = DatabasePerfContext(parsed_output[self.PERF_CON], 0, False)
+ # create the data-sources dictionary
+ data_sources = {
+ DataSource.Type.DB_OPTIONS: [db_options],
+ DataSource.Type.LOG: [db_logs],
+ DataSource.Type.TIME_SERIES: [db_log_stats, db_perf_context],
+ }
+ # Create the ODS STATS object
+ if self.ods_args:
+ key_prefix = ""
+ if "key_prefix" in self.ods_args:
+ key_prefix = self.ods_args["key_prefix"]
+ data_sources[DataSource.Type.TIME_SERIES].append(
+ OdsStatsFetcher(
+ self.ods_args["client_script"],
+ self.ods_args["entity"],
+ experiment_start_time,
+ experiment_end_time,
+ key_prefix,
+ )
+ )
+ # return the experiment's data-sources and throughput
+ return data_sources, parsed_output[self.THROUGHPUT]
diff --git a/src/rocksdb/tools/advisor/advisor/db_config_optimizer.py b/src/rocksdb/tools/advisor/advisor/db_config_optimizer.py
new file mode 100644
index 000000000..413778478
--- /dev/null
+++ b/src/rocksdb/tools/advisor/advisor/db_config_optimizer.py
@@ -0,0 +1,293 @@
+# Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+# This source code is licensed under both the GPLv2 (found in the
+# COPYING file in the root directory) and Apache 2.0 License
+# (found in the LICENSE.Apache file in the root directory).
+
+import copy
+import random
+
+from advisor.db_log_parser import NO_COL_FAMILY
+from advisor.db_options_parser import DatabaseOptions
+from advisor.rule_parser import Suggestion
+
+
+class ConfigOptimizer:
+ SCOPE = "scope"
+ SUGG_VAL = "suggested values"
+
+ @staticmethod
+ def apply_action_on_value(old_value, action, suggested_values):
+ chosen_sugg_val = None
+ if suggested_values:
+ chosen_sugg_val = random.choice(list(suggested_values))
+ new_value = None
+ if action is Suggestion.Action.set or not old_value:
+ assert chosen_sugg_val
+ new_value = chosen_sugg_val
+ else:
+ # For increase/decrease actions, currently the code tries to make
+ # a 30% change in the option's value per iteration. An addend is
+ # also present (+1 or -1) to handle the cases when the option's
+ # old value was 0 or the final int() conversion suppressed the 30%
+ # change made to the option
+ old_value = float(old_value)
+ mul = 0
+ add = 0
+ if action is Suggestion.Action.increase:
+ if old_value < 0:
+ mul = 0.7
+ add = 2
+ else:
+ mul = 1.3
+ add = 2
+ elif action is Suggestion.Action.decrease:
+ if old_value < 0:
+ mul = 1.3
+ add = -2
+ else:
+ mul = 0.7
+ add = -2
+ new_value = int(old_value * mul + add)
+ return new_value
+
+ @staticmethod
+ def improve_db_config(options, rule, suggestions_dict):
+ # this method takes ONE 'rule' and applies all its suggestions on the
+ # appropriate options
+ required_options = []
+ rule_suggestions = []
+ for sugg_name in rule.get_suggestions():
+ option = suggestions_dict[sugg_name].option
+ action = suggestions_dict[sugg_name].action
+ # A Suggestion in the rules spec must have the 'option' and
+ # 'action' fields defined, always call perform_checks() method
+ # after parsing the rules file using RulesSpec
+ assert option
+ assert action
+ required_options.append(option)
+ rule_suggestions.append(suggestions_dict[sugg_name])
+ current_config = options.get_options(required_options)
+ # Create the updated configuration from the rule's suggestions
+ updated_config = {}
+ for sugg in rule_suggestions:
+ # case: when the option is not present in the current configuration
+ if sugg.option not in current_config:
+ try:
+ new_value = ConfigOptimizer.apply_action_on_value(
+ None, sugg.action, sugg.suggested_values
+ )
+ if sugg.option not in updated_config:
+ updated_config[sugg.option] = {}
+ if DatabaseOptions.is_misc_option(sugg.option):
+ # this suggestion is on an option that is not yet
+ # supported by the Rocksdb OPTIONS file and so it is
+ # not prefixed by a section type.
+ updated_config[sugg.option][NO_COL_FAMILY] = new_value
+ else:
+ for col_fam in rule.get_trigger_column_families():
+ updated_config[sugg.option][col_fam] = new_value
+ except AssertionError:
+ print(
+ "WARNING(ConfigOptimizer): provide suggested_values "
+ + "for "
+ + sugg.option
+ )
+ continue
+ # case: when the option is present in the current configuration
+ if NO_COL_FAMILY in current_config[sugg.option]:
+ old_value = current_config[sugg.option][NO_COL_FAMILY]
+ try:
+ new_value = ConfigOptimizer.apply_action_on_value(
+ old_value, sugg.action, sugg.suggested_values
+ )
+ if sugg.option not in updated_config:
+ updated_config[sugg.option] = {}
+ updated_config[sugg.option][NO_COL_FAMILY] = new_value
+ except AssertionError:
+ print(
+ "WARNING(ConfigOptimizer): provide suggested_values "
+ + "for "
+ + sugg.option
+ )
+ else:
+ for col_fam in rule.get_trigger_column_families():
+ old_value = None
+ if col_fam in current_config[sugg.option]:
+ old_value = current_config[sugg.option][col_fam]
+ try:
+ new_value = ConfigOptimizer.apply_action_on_value(
+ old_value, sugg.action, sugg.suggested_values
+ )
+ if sugg.option not in updated_config:
+ updated_config[sugg.option] = {}
+ updated_config[sugg.option][col_fam] = new_value
+ except AssertionError:
+ print(
+ "WARNING(ConfigOptimizer): provide "
+ + "suggested_values for "
+ + sugg.option
+ )
+ return current_config, updated_config
+
+ @staticmethod
+ def pick_rule_to_apply(rules, last_rule_name, rules_tried, backtrack):
+ if not rules:
+ print("\nNo more rules triggered!")
+ return None
+ # if the last rule provided an improvement in the database performance,
+ # and it was triggered again (i.e. it is present in 'rules'), then pick
+ # the same rule for this iteration too.
+ if last_rule_name and not backtrack:
+ for rule in rules:
+ if rule.name == last_rule_name:
+ return rule
+ # there was no previous rule OR the previous rule did not improve db
+ # performance OR it was not triggered for this iteration,
+ # then pick another rule that has not been tried yet
+ for rule in rules:
+ if rule.name not in rules_tried:
+ return rule
+ print("\nAll rules have been exhausted")
+ return None
+
+ @staticmethod
+ def apply_suggestions(
+ triggered_rules,
+ current_rule_name,
+ rules_tried,
+ backtrack,
+ curr_options,
+ suggestions_dict,
+ ):
+ curr_rule = ConfigOptimizer.pick_rule_to_apply(
+ triggered_rules, current_rule_name, rules_tried, backtrack
+ )
+ if not curr_rule:
+ return tuple([None] * 4)
+ # if a rule has been picked for improving db_config, update rules_tried
+ rules_tried.add(curr_rule.name)
+ # get updated config based on the picked rule
+ curr_conf, updated_conf = ConfigOptimizer.improve_db_config(
+ curr_options, curr_rule, suggestions_dict
+ )
+ conf_diff = DatabaseOptions.get_options_diff(curr_conf, updated_conf)
+ if not conf_diff: # the current and updated configs are the same
+ (
+ curr_rule,
+ rules_tried,
+ curr_conf,
+ updated_conf,
+ ) = ConfigOptimizer.apply_suggestions(
+ triggered_rules,
+ None,
+ rules_tried,
+ backtrack,
+ curr_options,
+ suggestions_dict,
+ )
+ print("returning from apply_suggestions")
+ return (curr_rule, rules_tried, curr_conf, updated_conf)
+
+ # TODO(poojam23): check if this method is required or can we directly set
+ # the config equal to the curr_config
+ @staticmethod
+ def get_backtrack_config(curr_config, updated_config):
+ diff = DatabaseOptions.get_options_diff(curr_config, updated_config)
+ bt_config = {}
+ for option in diff:
+ bt_config[option] = {}
+ for col_fam in diff[option]:
+ bt_config[option][col_fam] = diff[option][col_fam][0]
+ print(bt_config)
+ return bt_config
+
+ def __init__(self, bench_runner, db_options, rule_parser, base_db):
+ self.bench_runner = bench_runner
+ self.db_options = db_options
+ self.rule_parser = rule_parser
+ self.base_db_path = base_db
+
+ def run(self):
+ # In every iteration of this method's optimization loop we pick ONE
+ # RULE from all the triggered rules and apply all its suggestions to
+ # the appropriate options.
+ # bootstrapping the optimizer
+ print("Bootstrapping optimizer:")
+ options = copy.deepcopy(self.db_options)
+ old_data_sources, old_metric = self.bench_runner.run_experiment(
+ options, self.base_db_path
+ )
+ print("Initial metric: " + str(old_metric))
+ self.rule_parser.load_rules_from_spec()
+ self.rule_parser.perform_section_checks()
+ triggered_rules = self.rule_parser.get_triggered_rules(
+ old_data_sources, options.get_column_families()
+ )
+ print("\nTriggered:")
+ self.rule_parser.print_rules(triggered_rules)
+ backtrack = False
+ rules_tried = set()
+ (
+ curr_rule,
+ rules_tried,
+ curr_conf,
+ updated_conf,
+ ) = ConfigOptimizer.apply_suggestions(
+ triggered_rules,
+ None,
+ rules_tried,
+ backtrack,
+ options,
+ self.rule_parser.get_suggestions_dict(),
+ )
+ # the optimizer loop
+ while curr_rule:
+ print("\nRule picked for next iteration:")
+ print(curr_rule.name)
+ print("\ncurrent config:")
+ print(curr_conf)
+ print("updated config:")
+ print(updated_conf)
+ options.update_options(updated_conf)
+ # run bench_runner with updated config
+ new_data_sources, new_metric = self.bench_runner.run_experiment(
+ options, self.base_db_path
+ )
+ print("\nnew metric: " + str(new_metric))
+ backtrack = not self.bench_runner.is_metric_better(new_metric, old_metric)
+ # update triggered_rules, metric, data_sources, if required
+ if backtrack:
+ # revert changes to options config
+ print("\nBacktracking to previous configuration")
+ backtrack_conf = ConfigOptimizer.get_backtrack_config(
+ curr_conf, updated_conf
+ )
+ options.update_options(backtrack_conf)
+ else:
+ # run advisor on new data sources
+ self.rule_parser.load_rules_from_spec() # reboot the advisor
+ self.rule_parser.perform_section_checks()
+ triggered_rules = self.rule_parser.get_triggered_rules(
+ new_data_sources, options.get_column_families()
+ )
+ print("\nTriggered:")
+ self.rule_parser.print_rules(triggered_rules)
+ old_metric = new_metric
+ old_data_sources = new_data_sources
+ rules_tried = set()
+ # pick rule to work on and set curr_rule to that
+ (
+ curr_rule,
+ rules_tried,
+ curr_conf,
+ updated_conf,
+ ) = ConfigOptimizer.apply_suggestions(
+ triggered_rules,
+ curr_rule.name,
+ rules_tried,
+ backtrack,
+ options,
+ self.rule_parser.get_suggestions_dict(),
+ )
+ # return the final database options configuration
+ return options
diff --git a/src/rocksdb/tools/advisor/advisor/db_log_parser.py b/src/rocksdb/tools/advisor/advisor/db_log_parser.py
new file mode 100644
index 000000000..9ba541fc3
--- /dev/null
+++ b/src/rocksdb/tools/advisor/advisor/db_log_parser.py
@@ -0,0 +1,134 @@
+# Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+# This source code is licensed under both the GPLv2 (found in the
+# COPYING file in the root directory) and Apache 2.0 License
+# (found in the LICENSE.Apache file in the root directory).
+
+import glob
+import re
+import time
+from abc import ABC, abstractmethod
+from calendar import timegm
+from enum import Enum
+
+
+NO_COL_FAMILY = "DB_WIDE"
+
+
+class DataSource(ABC):
+ class Type(Enum):
+ LOG = 1
+ DB_OPTIONS = 2
+ TIME_SERIES = 3
+
+ def __init__(self, type):
+ self.type = type
+
+ @abstractmethod
+ def check_and_trigger_conditions(self, conditions):
+ pass
+
+
+class Log:
+ @staticmethod
+ def is_new_log(log_line):
+ # The assumption is that a new log will start with a date printed in
+ # the below regex format.
+ date_regex = "\d{4}/\d{2}/\d{2}-\d{2}:\d{2}:\d{2}\.\d{6}" # noqa
+ return re.match(date_regex, log_line)
+
+ def __init__(self, log_line, column_families):
+ token_list = log_line.strip().split()
+ self.time = token_list[0]
+ self.context = token_list[1]
+ self.message = " ".join(token_list[2:])
+ self.column_family = None
+ # example log for 'default' column family:
+ # "2018/07/25-17:29:05.176080 7f969de68700 [db/compaction_job.cc:1634]
+ # [default] [JOB 3] Compacting 24@0 + 16@1 files to L1, score 6.00\n"
+ for col_fam in column_families:
+ search_for_str = "\[" + col_fam + "\]" # noqa
+ if re.search(search_for_str, self.message):
+ self.column_family = col_fam
+ break
+ if not self.column_family:
+ self.column_family = NO_COL_FAMILY
+
+ def get_human_readable_time(self):
+ # example from a log line: '2018/07/25-11:25:45.782710'
+ return self.time
+
+ def get_column_family(self):
+ return self.column_family
+
+ def get_context(self):
+ return self.context
+
+ def get_message(self):
+ return self.message
+
+ def append_message(self, remaining_log):
+ self.message = self.message + "\n" + remaining_log.strip()
+
+ def get_timestamp(self):
+ # example: '2018/07/25-11:25:45.782710' will be converted to the GMT
+ # Unix timestamp 1532517945 (note: this method assumes that self.time
+ # is in GMT)
+ hr_time = self.time + "GMT"
+ timestamp = timegm(time.strptime(hr_time, "%Y/%m/%d-%H:%M:%S.%f%Z"))
+ return timestamp
+
+ def __repr__(self):
+ return (
+ "time: "
+ + self.time
+ + "; context: "
+ + self.context
+ + "; col_fam: "
+ + self.column_family
+ + "; message: "
+ + self.message
+ )
+
+
+class DatabaseLogs(DataSource):
+ def __init__(self, logs_path_prefix, column_families):
+ super().__init__(DataSource.Type.LOG)
+ self.logs_path_prefix = logs_path_prefix
+ self.column_families = column_families
+
+ def trigger_conditions_for_log(self, conditions, log):
+ # For a LogCondition object, trigger is:
+ # Dict[column_family_name, List[Log]]. This explains why the condition
+ # was triggered and for which column families.
+ for cond in conditions:
+ if re.search(cond.regex, log.get_message(), re.IGNORECASE):
+ trigger = cond.get_trigger()
+ if not trigger:
+ trigger = {}
+ if log.get_column_family() not in trigger:
+ trigger[log.get_column_family()] = []
+ trigger[log.get_column_family()].append(log)
+ cond.set_trigger(trigger)
+
+ def check_and_trigger_conditions(self, conditions):
+ for file_name in glob.glob(self.logs_path_prefix + "*"):
+ # TODO(poojam23): find a way to distinguish between log files
+ # - generated in the current experiment but are labeled 'old'
+ # because they LOGs exceeded the file size limit AND
+ # - generated in some previous experiment that are also labeled
+ # 'old' and were not deleted for some reason
+ if re.search("old", file_name, re.IGNORECASE):
+ continue
+ with open(file_name, "r") as db_logs:
+ new_log = None
+ for line in db_logs:
+ if Log.is_new_log(line):
+ if new_log:
+ self.trigger_conditions_for_log(conditions, new_log)
+ new_log = Log(line, self.column_families)
+ else:
+ # To account for logs split into multiple lines
+ new_log.append_message(line)
+ # Check for the last log in the file.
+ if new_log:
+ self.trigger_conditions_for_log(conditions, new_log)
diff --git a/src/rocksdb/tools/advisor/advisor/db_options_parser.py b/src/rocksdb/tools/advisor/advisor/db_options_parser.py
new file mode 100644
index 000000000..062aeeec4
--- /dev/null
+++ b/src/rocksdb/tools/advisor/advisor/db_options_parser.py
@@ -0,0 +1,348 @@
+# Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+# This source code is licensed under both the GPLv2 (found in the
+# COPYING file in the root directory) and Apache 2.0 License
+# (found in the LICENSE.Apache file in the root directory).
+
+import copy
+import os
+
+from advisor.db_log_parser import DataSource, NO_COL_FAMILY
+from advisor.ini_parser import IniParser
+
+
+class OptionsSpecParser(IniParser):
+ @staticmethod
+ def is_new_option(line):
+ return "=" in line
+
+ @staticmethod
+ def get_section_type(line):
+ """
+ Example section header: [TableOptions/BlockBasedTable "default"]
+ Here ConfigurationOptimizer returned would be
+ 'TableOptions.BlockBasedTable'
+ """
+ section_path = line.strip()[1:-1].split()[0]
+ section_type = ".".join(section_path.split("/"))
+ return section_type
+
+ @staticmethod
+ def get_section_name(line):
+ # example: get_section_name('[CFOptions "default"]')
+ token_list = line.strip()[1:-1].split('"')
+ # token_list = ['CFOptions', 'default', '']
+ if len(token_list) < 3:
+ return None
+ return token_list[1] # return 'default'
+
+ @staticmethod
+ def get_section_str(section_type, section_name):
+ # Example:
+ # Case 1: get_section_str('DBOptions', NO_COL_FAMILY)
+ # Case 2: get_section_str('TableOptions.BlockBasedTable', 'default')
+ section_type = "/".join(section_type.strip().split("."))
+ # Case 1: section_type = 'DBOptions'
+ # Case 2: section_type = 'TableOptions/BlockBasedTable'
+ section_str = "[" + section_type
+ if section_name == NO_COL_FAMILY:
+ # Case 1: '[DBOptions]'
+ return section_str + "]"
+ else:
+ # Case 2: '[TableOptions/BlockBasedTable "default"]'
+ return section_str + ' "' + section_name + '"]'
+
+ @staticmethod
+ def get_option_str(key, values):
+ option_str = key + "="
+ # get_option_str('db_log_dir', None), returns 'db_log_dir='
+ if values:
+ # example:
+ # get_option_str('max_bytes_for_level_multiplier_additional',
+ # [1,1,1,1,1,1,1]), returned string:
+ # 'max_bytes_for_level_multiplier_additional=1:1:1:1:1:1:1'
+ if isinstance(values, list):
+ for value in values:
+ option_str += str(value) + ":"
+ option_str = option_str[:-1]
+ else:
+ # example: get_option_str('write_buffer_size', 1048576)
+ # returned string: 'write_buffer_size=1048576'
+ option_str += str(values)
+ return option_str
+
+
+class DatabaseOptions(DataSource):
+ @staticmethod
+ def is_misc_option(option_name):
+ # these are miscellaneous options that are not yet supported by the
+ # Rocksdb options file, hence they are not prefixed with any section
+ # name
+ return "." not in option_name
+
+ @staticmethod
+ def get_options_diff(opt_old, opt_new):
+ # type: Dict[option, Dict[col_fam, value]] X 2 ->
+ # Dict[option, Dict[col_fam, Tuple(old_value, new_value)]]
+ # note: diff should contain a tuple of values only if they are
+ # different from each other
+ options_union = set(opt_old.keys()).union(set(opt_new.keys()))
+ diff = {}
+ for opt in options_union:
+ diff[opt] = {}
+ # if option in options_union, then it must be in one of the configs
+ if opt not in opt_old:
+ for col_fam in opt_new[opt]:
+ diff[opt][col_fam] = (None, opt_new[opt][col_fam])
+ elif opt not in opt_new:
+ for col_fam in opt_old[opt]:
+ diff[opt][col_fam] = (opt_old[opt][col_fam], None)
+ else:
+ for col_fam in opt_old[opt]:
+ if col_fam in opt_new[opt]:
+ if opt_old[opt][col_fam] != opt_new[opt][col_fam]:
+ diff[opt][col_fam] = (
+ opt_old[opt][col_fam],
+ opt_new[opt][col_fam],
+ )
+ else:
+ diff[opt][col_fam] = (opt_old[opt][col_fam], None)
+ for col_fam in opt_new[opt]:
+ if col_fam in opt_old[opt]:
+ if opt_old[opt][col_fam] != opt_new[opt][col_fam]:
+ diff[opt][col_fam] = (
+ opt_old[opt][col_fam],
+ opt_new[opt][col_fam],
+ )
+ else:
+ diff[opt][col_fam] = (None, opt_new[opt][col_fam])
+ if not diff[opt]:
+ diff.pop(opt)
+ return diff
+
+ def __init__(self, rocksdb_options, misc_options=None):
+ super().__init__(DataSource.Type.DB_OPTIONS)
+ # The options are stored in the following data structure:
+ # Dict[section_type, Dict[section_name, Dict[option_name, value]]]
+ self.options_dict = None
+ self.column_families = None
+ # Load the options from the given file to a dictionary.
+ self.load_from_source(rocksdb_options)
+ # Setup the miscellaneous options expected to be List[str], where each
+ # element in the List has the format "<option_name>=<option_value>"
+ # These options are the ones that are not yet supported by the Rocksdb
+ # OPTIONS file, so they are provided separately
+ self.setup_misc_options(misc_options)
+
+ def setup_misc_options(self, misc_options):
+ self.misc_options = {}
+ if misc_options:
+ for option_pair_str in misc_options:
+ option_name = option_pair_str.split("=")[0].strip()
+ option_value = option_pair_str.split("=")[1].strip()
+ self.misc_options[option_name] = option_value
+
+ def load_from_source(self, options_path):
+ self.options_dict = {}
+ with open(options_path, "r") as db_options:
+ for line in db_options:
+ line = OptionsSpecParser.remove_trailing_comment(line)
+ if not line:
+ continue
+ if OptionsSpecParser.is_section_header(line):
+ curr_sec_type = OptionsSpecParser.get_section_type(line)
+ curr_sec_name = OptionsSpecParser.get_section_name(line)
+ if curr_sec_type not in self.options_dict:
+ self.options_dict[curr_sec_type] = {}
+ if not curr_sec_name:
+ curr_sec_name = NO_COL_FAMILY
+ self.options_dict[curr_sec_type][curr_sec_name] = {}
+ # example: if the line read from the Rocksdb OPTIONS file
+ # is [CFOptions "default"], then the section type is
+ # CFOptions and 'default' is the name of a column family
+ # that for this database, so it's added to the list of
+ # column families stored in this object
+ if curr_sec_type == "CFOptions":
+ if not self.column_families:
+ self.column_families = []
+ self.column_families.append(curr_sec_name)
+ elif OptionsSpecParser.is_new_option(line):
+ key, value = OptionsSpecParser.get_key_value_pair(line)
+ self.options_dict[curr_sec_type][curr_sec_name][key] = value
+ else:
+ error = "Not able to parse line in Options file."
+ OptionsSpecParser.exit_with_parse_error(line, error)
+
+ def get_misc_options(self):
+ # these are options that are not yet supported by the Rocksdb OPTIONS
+ # file, hence they are provided and stored separately
+ return self.misc_options
+
+ def get_column_families(self):
+ return self.column_families
+
+ def get_all_options(self):
+ # This method returns all the options that are stored in this object as
+ # a: Dict[<sec_type>.<option_name>: Dict[col_fam, option_value]]
+ all_options = []
+ # Example: in the section header '[CFOptions "default"]' read from the
+ # OPTIONS file, sec_type='CFOptions'
+ for sec_type in self.options_dict:
+ for col_fam in self.options_dict[sec_type]:
+ for opt_name in self.options_dict[sec_type][col_fam]:
+ option = sec_type + "." + opt_name
+ all_options.append(option)
+ all_options.extend(list(self.misc_options.keys()))
+ return self.get_options(all_options)
+
+ def get_options(self, reqd_options):
+ # type: List[str] -> Dict[str, Dict[str, Any]]
+ # List[option] -> Dict[option, Dict[col_fam, value]]
+ reqd_options_dict = {}
+ for option in reqd_options:
+ if DatabaseOptions.is_misc_option(option):
+ # the option is not prefixed by '<section_type>.' because it is
+ # not yet supported by the Rocksdb OPTIONS file; so it has to
+ # be fetched from the misc_options dictionary
+ if option not in self.misc_options:
+ continue
+ if option not in reqd_options_dict:
+ reqd_options_dict[option] = {}
+ reqd_options_dict[option][NO_COL_FAMILY] = self.misc_options[option]
+ else:
+ # Example: option = 'TableOptions.BlockBasedTable.block_align'
+ # then, sec_type = 'TableOptions.BlockBasedTable'
+ sec_type = ".".join(option.split(".")[:-1])
+ # opt_name = 'block_align'
+ opt_name = option.split(".")[-1]
+ if sec_type not in self.options_dict:
+ continue
+ for col_fam in self.options_dict[sec_type]:
+ if opt_name in self.options_dict[sec_type][col_fam]:
+ if option not in reqd_options_dict:
+ reqd_options_dict[option] = {}
+ reqd_options_dict[option][col_fam] = self.options_dict[
+ sec_type
+ ][col_fam][opt_name]
+ return reqd_options_dict
+
+ def update_options(self, options):
+ # An example 'options' object looks like:
+ # {'DBOptions.max_background_jobs': {NO_COL_FAMILY: 2},
+ # 'CFOptions.write_buffer_size': {'default': 1048576, 'cf_A': 128000},
+ # 'bloom_bits': {NO_COL_FAMILY: 4}}
+ for option in options:
+ if DatabaseOptions.is_misc_option(option):
+ # this is a misc_option i.e. an option that is not yet
+ # supported by the Rocksdb OPTIONS file, so it is not prefixed
+ # by '<section_type>.' and must be stored in the separate
+ # misc_options dictionary
+ if NO_COL_FAMILY not in options[option]:
+ print(
+ "WARNING(DatabaseOptions.update_options): not "
+ + "updating option "
+ + option
+ + " because it is in "
+ + "misc_option format but its scope is not "
+ + NO_COL_FAMILY
+ + ". Check format of option."
+ )
+ continue
+ self.misc_options[option] = options[option][NO_COL_FAMILY]
+ else:
+ sec_name = ".".join(option.split(".")[:-1])
+ opt_name = option.split(".")[-1]
+ if sec_name not in self.options_dict:
+ self.options_dict[sec_name] = {}
+ for col_fam in options[option]:
+ # if the option is not already present in the dictionary,
+ # it will be inserted, else it will be updated to the new
+ # value
+ if col_fam not in self.options_dict[sec_name]:
+ self.options_dict[sec_name][col_fam] = {}
+ self.options_dict[sec_name][col_fam][opt_name] = copy.deepcopy(
+ options[option][col_fam]
+ )
+
+ def generate_options_config(self, nonce):
+ # this method generates a Rocksdb OPTIONS file in the INI format from
+ # the options stored in self.options_dict
+ this_path = os.path.abspath(os.path.dirname(__file__))
+ file_name = "../temp/OPTIONS_" + str(nonce) + ".tmp"
+ file_path = os.path.join(this_path, file_name)
+ with open(file_path, "w") as fp:
+ for section in self.options_dict:
+ for col_fam in self.options_dict[section]:
+ fp.write(OptionsSpecParser.get_section_str(section, col_fam) + "\n")
+ for option in self.options_dict[section][col_fam]:
+ values = self.options_dict[section][col_fam][option]
+ fp.write(
+ OptionsSpecParser.get_option_str(option, values) + "\n"
+ )
+ fp.write("\n")
+ return file_path
+
+ def check_and_trigger_conditions(self, conditions):
+ for cond in conditions:
+ reqd_options_dict = self.get_options(cond.options)
+ # This contains the indices of options that are specific to some
+ # column family and are not database-wide options.
+ incomplete_option_ix = []
+ options = []
+ missing_reqd_option = False
+ for ix, option in enumerate(cond.options):
+ if option not in reqd_options_dict:
+ print(
+ "WARNING(DatabaseOptions.check_and_trigger): "
+ + "skipping condition "
+ + cond.name
+ + " because it "
+ "requires option "
+ + option
+ + " but this option is"
+ + " not available"
+ )
+ missing_reqd_option = True
+ break # required option is absent
+ if NO_COL_FAMILY in reqd_options_dict[option]:
+ options.append(reqd_options_dict[option][NO_COL_FAMILY])
+ else:
+ options.append(None)
+ incomplete_option_ix.append(ix)
+
+ if missing_reqd_option:
+ continue
+
+ # if all the options are database-wide options
+ if not incomplete_option_ix:
+ try:
+ if eval(cond.eval_expr):
+ cond.set_trigger({NO_COL_FAMILY: options})
+ except Exception as e:
+ print("WARNING(DatabaseOptions) check_and_trigger:" + str(e))
+ continue
+
+ # for all the options that are not database-wide, we look for their
+ # values specific to column families
+ col_fam_options_dict = {}
+ for col_fam in self.column_families:
+ present = True
+ for ix in incomplete_option_ix:
+ option = cond.options[ix]
+ if col_fam not in reqd_options_dict[option]:
+ present = False
+ break
+ options[ix] = reqd_options_dict[option][col_fam]
+ if present:
+ try:
+ if eval(cond.eval_expr):
+ col_fam_options_dict[col_fam] = copy.deepcopy(options)
+ except Exception as e:
+ print("WARNING(DatabaseOptions) check_and_trigger: " + str(e))
+ # Trigger for an OptionCondition object is of the form:
+ # Dict[col_fam_name: List[option_value]]
+ # where col_fam_name is the name of a column family for which
+ # 'eval_expr' evaluated to True and List[option_value] is the list
+ # of values of the options specified in the condition's 'options'
+ # field
+ if col_fam_options_dict:
+ cond.set_trigger(col_fam_options_dict)
diff --git a/src/rocksdb/tools/advisor/advisor/db_stats_fetcher.py b/src/rocksdb/tools/advisor/advisor/db_stats_fetcher.py
new file mode 100755
index 000000000..30d1ad8b3
--- /dev/null
+++ b/src/rocksdb/tools/advisor/advisor/db_stats_fetcher.py
@@ -0,0 +1,346 @@
+# Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+# This source code is licensed under both the GPLv2 (found in the
+# COPYING file in the root directory) and Apache 2.0 License
+# (found in the LICENSE.Apache file in the root directory).
+
+import copy
+import glob
+import re
+import subprocess
+import time
+from typing import List
+
+from advisor.db_log_parser import Log
+from advisor.db_timeseries_parser import NO_ENTITY, TimeSeriesData
+
+
+class LogStatsParser(TimeSeriesData):
+ STATS = "STATISTICS:"
+
+ @staticmethod
+ def parse_log_line_for_stats(log_line):
+ # Example stat line (from LOG file):
+ # "rocksdb.db.get.micros P50 : 8.4 P95 : 21.8 P99 : 33.9 P100 : 92.0\n"
+ token_list = log_line.strip().split()
+ # token_list = ['rocksdb.db.get.micros', 'P50', ':', '8.4', 'P95', ':',
+ # '21.8', 'P99', ':', '33.9', 'P100', ':', '92.0']
+ stat_prefix = token_list[0] + "." # 'rocksdb.db.get.micros.'
+ stat_values = [token for token in token_list[1:] if token != ":"]
+ # stat_values = ['P50', '8.4', 'P95', '21.8', 'P99', '33.9', 'P100',
+ # '92.0']
+ stat_dict = {}
+ for ix, metric in enumerate(stat_values):
+ if ix % 2 == 0:
+ stat_name = stat_prefix + metric
+ stat_name = stat_name.lower() # Note: case insensitive names
+ else:
+ stat_dict[stat_name] = float(metric)
+ # stat_dict = {'rocksdb.db.get.micros.p50': 8.4,
+ # 'rocksdb.db.get.micros.p95': 21.8, 'rocksdb.db.get.micros.p99': 33.9,
+ # 'rocksdb.db.get.micros.p100': 92.0}
+ return stat_dict
+
+ def __init__(self, logs_path_prefix, stats_freq_sec):
+ super().__init__()
+ self.logs_file_prefix = logs_path_prefix
+ self.stats_freq_sec = stats_freq_sec
+ self.duration_sec = 60
+
+ def get_keys_from_conditions(self, conditions):
+ # Note: case insensitive stat names
+ reqd_stats = []
+ for cond in conditions:
+ for key in cond.keys:
+ key = key.lower()
+ # some keys are prepended with '[]' for OdsStatsFetcher to
+ # replace this with the appropriate key_prefix, remove these
+ # characters here since the LogStatsParser does not need
+ # a prefix
+ if key.startswith("[]"):
+ reqd_stats.append(key[2:])
+ else:
+ reqd_stats.append(key)
+ return reqd_stats
+
+ def add_to_timeseries(self, log, reqd_stats):
+ # this method takes in the Log object that contains the Rocksdb stats
+ # and a list of required stats, then it parses the stats line by line
+ # to fetch required stats and add them to the keys_ts object
+ # Example: reqd_stats = ['rocksdb.block.cache.hit.count',
+ # 'rocksdb.db.get.micros.p99']
+ # Let log.get_message() returns following string:
+ # "[WARN] [db/db_impl.cc:485] STATISTICS:\n
+ # rocksdb.block.cache.miss COUNT : 1459\n
+ # rocksdb.block.cache.hit COUNT : 37\n
+ # ...
+ # rocksdb.db.get.micros P50 : 15.6 P95 : 39.7 P99 : 62.6 P100 : 148.0\n
+ # ..."
+ new_lines = log.get_message().split("\n")
+ # let log_ts = 1532518219
+ log_ts = log.get_timestamp()
+ # example updates to keys_ts:
+ # keys_ts[NO_ENTITY]['rocksdb.db.get.micros.p99'][1532518219] = 62.6
+ # keys_ts[NO_ENTITY]['rocksdb.block.cache.hit.count'][1532518219] = 37
+ for line in new_lines[1:]: # new_lines[0] does not contain any stats
+ stats_on_line = self.parse_log_line_for_stats(line)
+ for stat in stats_on_line:
+ if stat in reqd_stats:
+ if stat not in self.keys_ts[NO_ENTITY]:
+ self.keys_ts[NO_ENTITY][stat] = {}
+ self.keys_ts[NO_ENTITY][stat][log_ts] = stats_on_line[stat]
+
+ def fetch_timeseries(self, reqd_stats):
+ # this method parses the Rocksdb LOG file and generates timeseries for
+ # each of the statistic in the list reqd_stats
+ self.keys_ts = {NO_ENTITY: {}}
+ for file_name in glob.glob(self.logs_file_prefix + "*"):
+ # TODO(poojam23): find a way to distinguish between 'old' log files
+ # from current and previous experiments, present in the same
+ # directory
+ if re.search("old", file_name, re.IGNORECASE):
+ continue
+ with open(file_name, "r") as db_logs:
+ new_log = None
+ for line in db_logs:
+ if Log.is_new_log(line):
+ if new_log and re.search(self.STATS, new_log.get_message()):
+ self.add_to_timeseries(new_log, reqd_stats)
+ new_log = Log(line, column_families=[])
+ else:
+ # To account for logs split into multiple lines
+ new_log.append_message(line)
+ # Check for the last log in the file.
+ if new_log and re.search(self.STATS, new_log.get_message()):
+ self.add_to_timeseries(new_log, reqd_stats)
+
+
+class DatabasePerfContext(TimeSeriesData):
+ # TODO(poojam23): check if any benchrunner provides PerfContext sampled at
+ # regular intervals
+ def __init__(self, perf_context_ts, stats_freq_sec, cumulative):
+ """
+ perf_context_ts is expected to be in the following format:
+ Dict[metric, Dict[timestamp, value]], where for
+ each (metric, timestamp) pair, the value is database-wide (i.e.
+ summed over all the threads involved)
+ if stats_freq_sec == 0, per-metric only one value is reported
+ """
+ super().__init__()
+ self.stats_freq_sec = stats_freq_sec
+ self.keys_ts = {NO_ENTITY: perf_context_ts}
+ if cumulative:
+ self.unaccumulate_metrics()
+
+ def unaccumulate_metrics(self):
+ # if the perf context metrics provided are cumulative in nature, this
+ # method can be used to convert them to a disjoint format
+ epoch_ts = copy.deepcopy(self.keys_ts)
+ for stat in self.keys_ts[NO_ENTITY]:
+ timeseries = sorted(
+ list(self.keys_ts[NO_ENTITY][stat].keys()), reverse=True
+ )
+ if len(timeseries) < 2:
+ continue
+ for ix, ts in enumerate(timeseries[:-1]):
+ epoch_ts[NO_ENTITY][stat][ts] = (
+ epoch_ts[NO_ENTITY][stat][ts]
+ - epoch_ts[NO_ENTITY][stat][timeseries[ix + 1]]
+ )
+ if epoch_ts[NO_ENTITY][stat][ts] < 0:
+ raise ValueError("DBPerfContext: really cumulative?")
+ # drop the smallest timestamp in the timeseries for this metric
+ epoch_ts[NO_ENTITY][stat].pop(timeseries[-1])
+ self.keys_ts = epoch_ts
+
+ def get_keys_from_conditions(self, conditions):
+ reqd_stats = []
+ for cond in conditions:
+ reqd_stats.extend([key.lower() for key in cond.keys])
+ return reqd_stats
+
+ def fetch_timeseries(self, statistics):
+ # this method is redundant for DatabasePerfContext because the __init__
+ # does the job of populating 'keys_ts'
+ pass
+
+
+class OdsStatsFetcher(TimeSeriesData):
+ # class constants
+ OUTPUT_FILE = "temp/stats_out.tmp"
+ ERROR_FILE = "temp/stats_err.tmp"
+ RAPIDO_COMMAND = "%s --entity=%s --key=%s --tstart=%s --tend=%s --showtime"
+
+ # static methods
+ @staticmethod
+ def _get_string_in_quotes(value):
+ return '"' + str(value) + '"'
+
+ @staticmethod
+ def _get_time_value_pair(pair_string):
+ # example pair_string: '[1532544591, 97.3653601828]'
+ pair_string = pair_string.replace("[", "")
+ pair_string = pair_string.replace("]", "")
+ pair = pair_string.split(",")
+ first = int(pair[0].strip())
+ second = float(pair[1].strip())
+ return [first, second]
+
+ @staticmethod
+ def _get_ods_cli_stime(start_time):
+ diff = int(time.time() - int(start_time))
+ stime = str(diff) + "_s"
+ return stime
+
+ def __init__(self, client, entities, start_time, end_time, key_prefix=None):
+ super().__init__()
+ self.client = client
+ self.entities = entities
+ self.start_time = start_time
+ self.end_time = end_time
+ self.key_prefix = key_prefix
+ self.stats_freq_sec = 60
+ self.duration_sec = 60
+
+ def execute_script(self, command):
+ print("executing...")
+ print(command)
+ out_file = open(self.OUTPUT_FILE, "w+")
+ err_file = open(self.ERROR_FILE, "w+")
+ subprocess.call(command, shell=True, stdout=out_file, stderr=err_file)
+ out_file.close()
+ err_file.close()
+
+ def parse_rapido_output(self):
+ # Output looks like the following:
+ # <entity_name>\t<key_name>\t[[ts, value], [ts, value], ...]
+ # ts = timestamp; value = value of key_name in entity_name at time ts
+ self.keys_ts = {}
+ with open(self.OUTPUT_FILE, "r") as fp:
+ for line in fp:
+ token_list = line.strip().split("\t")
+ entity = token_list[0]
+ key = token_list[1]
+ if entity not in self.keys_ts:
+ self.keys_ts[entity] = {}
+ if key not in self.keys_ts[entity]:
+ self.keys_ts[entity][key] = {}
+ list_of_lists = [
+ self._get_time_value_pair(pair_string)
+ for pair_string in token_list[2].split("],")
+ ]
+ value = {pair[0]: pair[1] for pair in list_of_lists}
+ self.keys_ts[entity][key] = value
+
+ def parse_ods_output(self):
+ # Output looks like the following:
+ # <entity_name>\t<key_name>\t<timestamp>\t<value>
+ # there is one line per (entity_name, key_name, timestamp)
+ self.keys_ts = {}
+ with open(self.OUTPUT_FILE, "r") as fp:
+ for line in fp:
+ token_list = line.split()
+ entity = token_list[0]
+ if entity not in self.keys_ts:
+ self.keys_ts[entity] = {}
+ key = token_list[1]
+ if key not in self.keys_ts[entity]:
+ self.keys_ts[entity][key] = {}
+ self.keys_ts[entity][key][token_list[2]] = token_list[3]
+
+ def fetch_timeseries(self, statistics):
+ # this method fetches the timeseries of required stats from the ODS
+ # service and populates the 'keys_ts' object appropriately
+ print("OdsStatsFetcher: fetching " + str(statistics))
+ if re.search("rapido", self.client, re.IGNORECASE):
+ command = self.RAPIDO_COMMAND % (
+ self.client,
+ self._get_string_in_quotes(self.entities),
+ self._get_string_in_quotes(",".join(statistics)),
+ self._get_string_in_quotes(self.start_time),
+ self._get_string_in_quotes(self.end_time),
+ )
+ # Run the tool and fetch the time-series data
+ self.execute_script(command)
+ # Parse output and populate the 'keys_ts' map
+ self.parse_rapido_output()
+ elif re.search("ods", self.client, re.IGNORECASE):
+ command = (
+ self.client
+ + " "
+ + "--stime="
+ + self._get_ods_cli_stime(self.start_time)
+ + " "
+ + self._get_string_in_quotes(self.entities)
+ + " "
+ + self._get_string_in_quotes(",".join(statistics))
+ )
+ # Run the tool and fetch the time-series data
+ self.execute_script(command)
+ # Parse output and populate the 'keys_ts' map
+ self.parse_ods_output()
+
+ def get_keys_from_conditions(self, conditions):
+ reqd_stats = []
+ for cond in conditions:
+ for key in cond.keys:
+ use_prefix = False
+ if key.startswith("[]"):
+ use_prefix = True
+ key = key[2:]
+ # TODO(poojam23): this is very hacky and needs to be improved
+ if key.startswith("rocksdb"):
+ key += ".60"
+ if use_prefix:
+ if not self.key_prefix:
+ print("Warning: OdsStatsFetcher might need key prefix")
+ print("for the key: " + key)
+ else:
+ key = self.key_prefix + "." + key
+ reqd_stats.append(key)
+ return reqd_stats
+
+ def fetch_rate_url(
+ self,
+ entities: List[str],
+ keys: List[str],
+ window_len: str,
+ percent: str,
+ display: bool,
+ ) -> str:
+ transform_desc = (
+ "rate(" + str(window_len) + ",duration=" + str(self.duration_sec)
+ )
+ if percent:
+ transform_desc = transform_desc + ",%)"
+ else:
+ transform_desc = transform_desc + ")"
+ if re.search("rapido", self.client, re.IGNORECASE):
+ command = self.RAPIDO_COMMAND + " --transform=%s --url=%s"
+ command = command % (
+ self.client,
+ self._get_string_in_quotes(",".join(entities)),
+ self._get_string_in_quotes(",".join(keys)),
+ self._get_string_in_quotes(self.start_time),
+ self._get_string_in_quotes(self.end_time),
+ self._get_string_in_quotes(transform_desc),
+ self._get_string_in_quotes(display),
+ )
+ elif re.search("ods", self.client, re.IGNORECASE):
+ command = (
+ self.client
+ + " "
+ + "--stime="
+ + self._get_ods_cli_stime(self.start_time)
+ + " "
+ + "--fburlonly "
+ + self._get_string_in_quotes(entities)
+ + " "
+ + self._get_string_in_quotes(",".join(keys))
+ + " "
+ + self._get_string_in_quotes(transform_desc)
+ )
+ self.execute_script(command)
+ url = ""
+ with open(self.OUTPUT_FILE, "r") as fp:
+ url = fp.readline()
+ return url
diff --git a/src/rocksdb/tools/advisor/advisor/db_timeseries_parser.py b/src/rocksdb/tools/advisor/advisor/db_timeseries_parser.py
new file mode 100644
index 000000000..5840d7b90
--- /dev/null
+++ b/src/rocksdb/tools/advisor/advisor/db_timeseries_parser.py
@@ -0,0 +1,203 @@
+# Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+# This source code is licensed under both the GPLv2 (found in the
+# COPYING file in the root directory) and Apache 2.0 License
+# (found in the LICENSE.Apache file in the root directory).
+
+import math
+from abc import abstractmethod
+from enum import Enum
+from typing import Dict
+
+from advisor.db_log_parser import DataSource
+
+
+NO_ENTITY = "ENTITY_PLACEHOLDER"
+
+
+class TimeSeriesData(DataSource):
+ class Behavior(Enum):
+ bursty = 1
+ evaluate_expression = 2
+
+ class AggregationOperator(Enum):
+ avg = 1
+ max = 2
+ min = 3
+ latest = 4
+ oldest = 5
+
+ def __init__(self):
+ super().__init__(DataSource.Type.TIME_SERIES)
+ self.keys_ts = None # Dict[entity, Dict[key, Dict[timestamp, value]]]
+ self.stats_freq_sec = None
+
+ @abstractmethod
+ def get_keys_from_conditions(self, conditions):
+ # This method takes in a list of time-series conditions; for each
+ # condition it manipulates the 'keys' in the way that is supported by
+ # the subclass implementing this method
+ pass
+
+ @abstractmethod
+ def fetch_timeseries(self, required_statistics):
+ # this method takes in a list of statistics and fetches the timeseries
+ # for each of them and populates the 'keys_ts' dictionary
+ pass
+
+ def fetch_burst_epochs(
+ self,
+ entities: str,
+ statistic: int,
+ window_sec: float,
+ threshold: bool,
+ percent: bool,
+ ) -> Dict[str, Dict[int, float]]:
+ # this method calculates the (percent) rate change in the 'statistic'
+ # for each entity (over 'window_sec' seconds) and returns the epochs
+ # where this rate change is greater than or equal to the 'threshold'
+ # value
+ if self.stats_freq_sec == 0:
+ # not time series data, cannot check for bursty behavior
+ return
+ if window_sec < self.stats_freq_sec:
+ window_sec = self.stats_freq_sec
+ # 'window_samples' is the number of windows to go back to
+ # compare the current window with, while calculating rate change.
+ window_samples = math.ceil(window_sec / self.stats_freq_sec)
+ burst_epochs = {}
+ # if percent = False:
+ # curr_val = value at window for which rate change is being calculated
+ # prev_val = value at window that is window_samples behind curr_window
+ # Then rate_without_percent =
+ # ((curr_val-prev_val)*duration_sec)/(curr_timestamp-prev_timestamp)
+ # if percent = True:
+ # rate_with_percent = (rate_without_percent * 100) / prev_val
+ # These calculations are in line with the rate() transform supported
+ # by ODS
+ for entity in entities:
+ if statistic not in self.keys_ts[entity]:
+ continue
+ timestamps = sorted(list(self.keys_ts[entity][statistic].keys()))
+ for ix in range(window_samples, len(timestamps), 1):
+ first_ts = timestamps[ix - window_samples]
+ last_ts = timestamps[ix]
+ first_val = self.keys_ts[entity][statistic][first_ts]
+ last_val = self.keys_ts[entity][statistic][last_ts]
+ diff = last_val - first_val
+ if percent:
+ diff = diff * 100 / first_val
+ rate = (diff * self.duration_sec) / (last_ts - first_ts)
+ # if the rate change is greater than the provided threshold,
+ # then the condition is triggered for entity at time 'last_ts'
+ if rate >= threshold:
+ if entity not in burst_epochs:
+ burst_epochs[entity] = {}
+ burst_epochs[entity][last_ts] = rate
+ return burst_epochs
+
+ def fetch_aggregated_values(self, entity, statistics, aggregation_op):
+ # this method performs the aggregation specified by 'aggregation_op'
+ # on the timeseries of 'statistics' for 'entity' and returns:
+ # Dict[statistic, aggregated_value]
+ result = {}
+ for stat in statistics:
+ if stat not in self.keys_ts[entity]:
+ continue
+ agg_val = None
+ if aggregation_op is self.AggregationOperator.latest:
+ latest_timestamp = max(list(self.keys_ts[entity][stat].keys()))
+ agg_val = self.keys_ts[entity][stat][latest_timestamp]
+ elif aggregation_op is self.AggregationOperator.oldest:
+ oldest_timestamp = min(list(self.keys_ts[entity][stat].keys()))
+ agg_val = self.keys_ts[entity][stat][oldest_timestamp]
+ elif aggregation_op is self.AggregationOperator.max:
+ agg_val = max(list(self.keys_ts[entity][stat].values()))
+ elif aggregation_op is self.AggregationOperator.min:
+ agg_val = min(list(self.keys_ts[entity][stat].values()))
+ elif aggregation_op is self.AggregationOperator.avg:
+ values = list(self.keys_ts[entity][stat].values())
+ agg_val = sum(values) / len(values)
+ result[stat] = agg_val
+ return result
+
+ def check_and_trigger_conditions(self, conditions):
+ # get the list of statistics that need to be fetched
+ reqd_keys = self.get_keys_from_conditions(conditions)
+ # fetch the required statistics and populate the map 'keys_ts'
+ self.fetch_timeseries(reqd_keys)
+ # Trigger the appropriate conditions
+ for cond in conditions:
+ complete_keys = self.get_keys_from_conditions([cond])
+ # Get the entities that have all statistics required by 'cond':
+ # an entity is checked for a given condition only if we possess all
+ # of the condition's 'keys' for that entity
+ entities_with_stats = []
+ for entity in self.keys_ts:
+ stat_missing = False
+ for stat in complete_keys:
+ if stat not in self.keys_ts[entity]:
+ stat_missing = True
+ break
+ if not stat_missing:
+ entities_with_stats.append(entity)
+ if not entities_with_stats:
+ continue
+ if cond.behavior is self.Behavior.bursty:
+ # for a condition that checks for bursty behavior, only one key
+ # should be present in the condition's 'keys' field
+ result = self.fetch_burst_epochs(
+ entities_with_stats,
+ complete_keys[0], # there should be only one key
+ cond.window_sec,
+ cond.rate_threshold,
+ True,
+ )
+ # Trigger in this case is:
+ # Dict[entity_name, Dict[timestamp, rate_change]]
+ # where the inner dictionary contains rate_change values when
+ # the rate_change >= threshold provided, with the
+ # corresponding timestamps
+ if result:
+ cond.set_trigger(result)
+ elif cond.behavior is self.Behavior.evaluate_expression:
+ self.handle_evaluate_expression(
+ cond, complete_keys, entities_with_stats
+ )
+
+ def handle_evaluate_expression(self, condition, statistics, entities):
+ trigger = {}
+ # check 'condition' for each of these entities
+ for entity in entities:
+ if hasattr(condition, "aggregation_op"):
+ # in this case, the aggregation operation is performed on each
+ # of the condition's 'keys' and then with aggregated values
+ # condition's 'expression' is evaluated; if it evaluates to
+ # True, then list of the keys values is added to the
+ # condition's trigger: Dict[entity_name, List[stats]]
+ result = self.fetch_aggregated_values(
+ entity, statistics, condition.aggregation_op
+ )
+ keys = [result[key] for key in statistics]
+ try:
+ if eval(condition.expression):
+ trigger[entity] = keys
+ except Exception as e:
+ print("WARNING(TimeSeriesData) check_and_trigger: " + str(e))
+ else:
+ # assumption: all stats have same series of timestamps
+ # this is similar to the above but 'expression' is evaluated at
+ # each timestamp, since there is no aggregation, and all the
+ # epochs are added to the trigger when the condition's
+ # 'expression' evaluated to true; so trigger is:
+ # Dict[entity, Dict[timestamp, List[stats]]]
+ for epoch in self.keys_ts[entity][statistics[0]].keys():
+ keys = [self.keys_ts[entity][key][epoch] for key in statistics]
+ try:
+ if eval(condition.expression):
+ if entity not in trigger:
+ trigger[entity] = {}
+ trigger[entity][epoch] = keys
+ except Exception as e:
+ print("WARNING(TimeSeriesData) check_and_trigger: " + str(e))
+ if trigger:
+ condition.set_trigger(trigger)
diff --git a/src/rocksdb/tools/advisor/advisor/ini_parser.py b/src/rocksdb/tools/advisor/advisor/ini_parser.py
new file mode 100644
index 000000000..3379ea3cd
--- /dev/null
+++ b/src/rocksdb/tools/advisor/advisor/ini_parser.py
@@ -0,0 +1,76 @@
+# Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+# This source code is licensed under both the GPLv2 (found in the
+# COPYING file in the root directory) and Apache 2.0 License
+# (found in the LICENSE.Apache file in the root directory).
+
+from enum import Enum
+
+
+class IniParser:
+ class Element(Enum):
+ rule = 1
+ cond = 2
+ sugg = 3
+ key_val = 4
+ comment = 5
+
+ @staticmethod
+ def remove_trailing_comment(line):
+ line = line.strip()
+ comment_start = line.find("#")
+ if comment_start > -1:
+ return line[:comment_start]
+ return line
+
+ @staticmethod
+ def is_section_header(line):
+ # A section header looks like: [Rule "my-new-rule"]. Essentially,
+ # a line that is in square-brackets.
+ line = line.strip()
+ if line.startswith("[") and line.endswith("]"):
+ return True
+ return False
+
+ @staticmethod
+ def get_section_name(line):
+ # For a section header: [Rule "my-new-rule"], this method will return
+ # "my-new-rule".
+ token_list = line.strip()[1:-1].split('"')
+ if len(token_list) < 3:
+ error = 'needed section header: [<section_type> "<section_name>"]'
+ raise ValueError("Parsing error: " + error + "\n" + line)
+ return token_list[1]
+
+ @staticmethod
+ def get_element(line):
+ line = IniParser.remove_trailing_comment(line)
+ if not line:
+ return IniParser.Element.comment
+ if IniParser.is_section_header(line):
+ if line.strip()[1:-1].startswith("Suggestion"):
+ return IniParser.Element.sugg
+ if line.strip()[1:-1].startswith("Rule"):
+ return IniParser.Element.rule
+ if line.strip()[1:-1].startswith("Condition"):
+ return IniParser.Element.cond
+ if "=" in line:
+ return IniParser.Element.key_val
+ error = "not a recognizable RulesSpec element"
+ raise ValueError("Parsing error: " + error + "\n" + line)
+
+ @staticmethod
+ def get_key_value_pair(line):
+ line = line.strip()
+ key = line.split("=")[0].strip()
+ value = "=".join(line.split("=")[1:])
+ if value == "": # if the option has no value
+ return (key, None)
+ values = IniParser.get_list_from_value(value)
+ if len(values) == 1:
+ return (key, value)
+ return (key, values)
+
+ @staticmethod
+ def get_list_from_value(value):
+ values = value.strip().split(":")
+ return values
diff --git a/src/rocksdb/tools/advisor/advisor/rule_parser.py b/src/rocksdb/tools/advisor/advisor/rule_parser.py
new file mode 100644
index 000000000..169a55363
--- /dev/null
+++ b/src/rocksdb/tools/advisor/advisor/rule_parser.py
@@ -0,0 +1,510 @@
+# Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+# This source code is licensed under both the GPLv2 (found in the
+# COPYING file in the root directory) and Apache 2.0 License
+# (found in the LICENSE.Apache file in the root directory).
+
+import re
+from abc import ABC, abstractmethod
+from enum import Enum
+
+from advisor.db_log_parser import DataSource, NO_COL_FAMILY
+from advisor.db_timeseries_parser import TimeSeriesData
+from advisor.ini_parser import IniParser
+
+
+class Section(ABC):
+ def __init__(self, name):
+ self.name = name
+
+ @abstractmethod
+ def set_parameter(self, key, value):
+ pass
+
+ @abstractmethod
+ def perform_checks(self):
+ pass
+
+
+class Rule(Section):
+ def __init__(self, name):
+ super().__init__(name)
+ self.conditions = None
+ self.suggestions = None
+ self.overlap_time_seconds = None
+ self.trigger_entities = None
+ self.trigger_column_families = None
+
+ def set_parameter(self, key, value):
+ # If the Rule is associated with a single suggestion/condition, then
+ # value will be a string and not a list. Hence, convert it to a single
+ # element list before storing it in self.suggestions or
+ # self.conditions.
+ if key == "conditions":
+ if isinstance(value, str):
+ self.conditions = [value]
+ else:
+ self.conditions = value
+ elif key == "suggestions":
+ if isinstance(value, str):
+ self.suggestions = [value]
+ else:
+ self.suggestions = value
+ elif key == "overlap_time_period":
+ self.overlap_time_seconds = value
+
+ def get_suggestions(self):
+ return self.suggestions
+
+ def perform_checks(self):
+ if not self.conditions or len(self.conditions) < 1:
+ raise ValueError(self.name + ": rule must have at least one condition")
+ if not self.suggestions or len(self.suggestions) < 1:
+ raise ValueError(self.name + ": rule must have at least one suggestion")
+ if self.overlap_time_seconds:
+ if len(self.conditions) != 2:
+ raise ValueError(
+ self.name
+ + ": rule must be associated with 2 conditions\
+ in order to check for a time dependency between them"
+ )
+ time_format = "^\d+[s|m|h|d]$" # noqa
+ if not re.match(time_format, self.overlap_time_seconds, re.IGNORECASE):
+ raise ValueError(
+ self.name + ": overlap_time_seconds format: \d+[s|m|h|d]"
+ )
+ else: # convert to seconds
+ in_seconds = int(self.overlap_time_seconds[:-1])
+ if self.overlap_time_seconds[-1] == "m":
+ in_seconds *= 60
+ elif self.overlap_time_seconds[-1] == "h":
+ in_seconds *= 60 * 60
+ elif self.overlap_time_seconds[-1] == "d":
+ in_seconds *= 24 * 60 * 60
+ self.overlap_time_seconds = in_seconds
+
+ def get_overlap_timestamps(self, key1_trigger_epochs, key2_trigger_epochs):
+ # this method takes in 2 timeseries i.e. timestamps at which the
+ # rule's 2 TIME_SERIES conditions were triggered and it finds
+ # (if present) the first pair of timestamps at which the 2 conditions
+ # were triggered within 'overlap_time_seconds' of each other
+ key1_lower_bounds = [
+ epoch - self.overlap_time_seconds for epoch in key1_trigger_epochs
+ ]
+ key1_lower_bounds.sort()
+ key2_trigger_epochs.sort()
+ trigger_ix = 0
+ overlap_pair = None
+ for key1_lb in key1_lower_bounds:
+ while key2_trigger_epochs[trigger_ix] < key1_lb and trigger_ix < len(
+ key2_trigger_epochs
+ ):
+ trigger_ix += 1
+ if trigger_ix >= len(key2_trigger_epochs):
+ break
+ if key2_trigger_epochs[trigger_ix] <= key1_lb + (
+ 2 * self.overlap_time_seconds
+ ):
+ overlap_pair = (
+ key2_trigger_epochs[trigger_ix],
+ key1_lb + self.overlap_time_seconds,
+ )
+ break
+ return overlap_pair
+
+ def get_trigger_entities(self):
+ return self.trigger_entities
+
+ def get_trigger_column_families(self):
+ return self.trigger_column_families
+
+ def is_triggered(self, conditions_dict, column_families):
+ if self.overlap_time_seconds:
+ condition1 = conditions_dict[self.conditions[0]]
+ condition2 = conditions_dict[self.conditions[1]]
+ if not (
+ condition1.get_data_source() is DataSource.Type.TIME_SERIES
+ and condition2.get_data_source() is DataSource.Type.TIME_SERIES
+ ):
+ raise ValueError(self.name + ": need 2 timeseries conditions")
+
+ map1 = condition1.get_trigger()
+ map2 = condition2.get_trigger()
+ if not (map1 and map2):
+ return False
+
+ self.trigger_entities = {}
+ is_triggered = False
+ entity_intersection = set(map1.keys()).intersection(set(map2.keys()))
+ for entity in entity_intersection:
+ overlap_timestamps_pair = self.get_overlap_timestamps(
+ list(map1[entity].keys()), list(map2[entity].keys())
+ )
+ if overlap_timestamps_pair:
+ self.trigger_entities[entity] = overlap_timestamps_pair
+ is_triggered = True
+ if is_triggered:
+ self.trigger_column_families = set(column_families)
+ return is_triggered
+ else:
+ all_conditions_triggered = True
+ self.trigger_column_families = set(column_families)
+ for cond_name in self.conditions:
+ cond = conditions_dict[cond_name]
+ if not cond.get_trigger():
+ all_conditions_triggered = False
+ break
+ if (
+ cond.get_data_source() is DataSource.Type.LOG
+ or cond.get_data_source() is DataSource.Type.DB_OPTIONS
+ ):
+ cond_col_fam = set(cond.get_trigger().keys())
+ if NO_COL_FAMILY in cond_col_fam:
+ cond_col_fam = set(column_families)
+ self.trigger_column_families = (
+ self.trigger_column_families.intersection(cond_col_fam)
+ )
+ elif cond.get_data_source() is DataSource.Type.TIME_SERIES:
+ cond_entities = set(cond.get_trigger().keys())
+ if self.trigger_entities is None:
+ self.trigger_entities = cond_entities
+ else:
+ self.trigger_entities = self.trigger_entities.intersection(
+ cond_entities
+ )
+ if not (self.trigger_entities or self.trigger_column_families):
+ all_conditions_triggered = False
+ break
+ if not all_conditions_triggered: # clean up if rule not triggered
+ self.trigger_column_families = None
+ self.trigger_entities = None
+ return all_conditions_triggered
+
+ def __repr__(self):
+ # Append conditions
+ rule_string = "Rule: " + self.name + " has conditions:: "
+ is_first = True
+ for cond in self.conditions:
+ if is_first:
+ rule_string += cond
+ is_first = False
+ else:
+ rule_string += " AND " + cond
+ # Append suggestions
+ rule_string += "\nsuggestions:: "
+ is_first = True
+ for sugg in self.suggestions:
+ if is_first:
+ rule_string += sugg
+ is_first = False
+ else:
+ rule_string += ", " + sugg
+ if self.trigger_entities:
+ rule_string += ", entities:: " + str(self.trigger_entities)
+ if self.trigger_column_families:
+ rule_string += ", col_fam:: " + str(self.trigger_column_families)
+ # Return constructed string
+ return rule_string
+
+
+class Suggestion(Section):
+ class Action(Enum):
+ set = 1
+ increase = 2
+ decrease = 3
+
+ def __init__(self, name):
+ super().__init__(name)
+ self.option = None
+ self.action = None
+ self.suggested_values = None
+ self.description = None
+
+ def set_parameter(self, key, value):
+ if key == "option":
+ # Note:
+ # case 1: 'option' is supported by Rocksdb OPTIONS file; in this
+ # case the option belongs to one of the sections in the config
+ # file and it's name is prefixed by "<section_type>."
+ # case 2: 'option' is not supported by Rocksdb OPTIONS file; the
+ # option is not expected to have the character '.' in its name
+ self.option = value
+ elif key == "action":
+ if self.option and not value:
+ raise ValueError(self.name + ": provide action for option")
+ self.action = self.Action[value]
+ elif key == "suggested_values":
+ if isinstance(value, str):
+ self.suggested_values = [value]
+ else:
+ self.suggested_values = value
+ elif key == "description":
+ self.description = value
+
+ def perform_checks(self):
+ if not self.description:
+ if not self.option:
+ raise ValueError(self.name + ": provide option or description")
+ if not self.action:
+ raise ValueError(self.name + ": provide action for option")
+ if self.action is self.Action.set and not self.suggested_values:
+ raise ValueError(self.name + ": provide suggested value for option")
+
+ def __repr__(self):
+ sugg_string = "Suggestion: " + self.name
+ if self.description:
+ sugg_string += " description : " + self.description
+ else:
+ sugg_string += " option : " + self.option + " action : " + self.action.name
+ if self.suggested_values:
+ sugg_string += " suggested_values : " + str(self.suggested_values)
+ return sugg_string
+
+
+class Condition(Section):
+ def __init__(self, name):
+ super().__init__(name)
+ self.data_source = None
+ self.trigger = None
+
+ def perform_checks(self):
+ if not self.data_source:
+ raise ValueError(self.name + ": condition not tied to data source")
+
+ def set_data_source(self, data_source):
+ self.data_source = data_source
+
+ def get_data_source(self):
+ return self.data_source
+
+ def reset_trigger(self):
+ self.trigger = None
+
+ def set_trigger(self, condition_trigger):
+ self.trigger = condition_trigger
+
+ def get_trigger(self):
+ return self.trigger
+
+ def is_triggered(self):
+ if self.trigger:
+ return True
+ return False
+
+ def set_parameter(self, key, value):
+ # must be defined by the subclass
+ raise NotImplementedError(self.name + ": provide source for condition")
+
+
+class LogCondition(Condition):
+ @classmethod
+ def create(cls, base_condition):
+ base_condition.set_data_source(DataSource.Type["LOG"])
+ base_condition.__class__ = cls
+ return base_condition
+
+ def set_parameter(self, key, value):
+ if key == "regex":
+ self.regex = value
+
+ def perform_checks(self):
+ super().perform_checks()
+ if not self.regex:
+ raise ValueError(self.name + ": provide regex for log condition")
+
+ def __repr__(self):
+ log_cond_str = "LogCondition: " + self.name
+ log_cond_str += " regex: " + self.regex
+ # if self.trigger:
+ # log_cond_str += (" trigger: " + str(self.trigger))
+ return log_cond_str
+
+
+class OptionCondition(Condition):
+ @classmethod
+ def create(cls, base_condition):
+ base_condition.set_data_source(DataSource.Type["DB_OPTIONS"])
+ base_condition.__class__ = cls
+ return base_condition
+
+ def set_parameter(self, key, value):
+ if key == "options":
+ if isinstance(value, str):
+ self.options = [value]
+ else:
+ self.options = value
+ elif key == "evaluate":
+ self.eval_expr = value
+
+ def perform_checks(self):
+ super().perform_checks()
+ if not self.options:
+ raise ValueError(self.name + ": options missing in condition")
+ if not self.eval_expr:
+ raise ValueError(self.name + ": expression missing in condition")
+
+ def __repr__(self):
+ opt_cond_str = "OptionCondition: " + self.name
+ opt_cond_str += " options: " + str(self.options)
+ opt_cond_str += " expression: " + self.eval_expr
+ if self.trigger:
+ opt_cond_str += " trigger: " + str(self.trigger)
+ return opt_cond_str
+
+
+class TimeSeriesCondition(Condition):
+ @classmethod
+ def create(cls, base_condition):
+ base_condition.set_data_source(DataSource.Type["TIME_SERIES"])
+ base_condition.__class__ = cls
+ return base_condition
+
+ def set_parameter(self, key, value):
+ if key == "keys":
+ if isinstance(value, str):
+ self.keys = [value]
+ else:
+ self.keys = value
+ elif key == "behavior":
+ self.behavior = TimeSeriesData.Behavior[value]
+ elif key == "rate_threshold":
+ self.rate_threshold = float(value)
+ elif key == "window_sec":
+ self.window_sec = int(value)
+ elif key == "evaluate":
+ self.expression = value
+ elif key == "aggregation_op":
+ self.aggregation_op = TimeSeriesData.AggregationOperator[value]
+
+ def perform_checks(self):
+ if not self.keys:
+ raise ValueError(self.name + ": specify timeseries key")
+ if not self.behavior:
+ raise ValueError(self.name + ": specify triggering behavior")
+ if self.behavior is TimeSeriesData.Behavior.bursty:
+ if not self.rate_threshold:
+ raise ValueError(self.name + ": specify rate burst threshold")
+ if not self.window_sec:
+ self.window_sec = 300 # default window length is 5 minutes
+ if len(self.keys) > 1:
+ raise ValueError(self.name + ": specify only one key")
+ elif self.behavior is TimeSeriesData.Behavior.evaluate_expression:
+ if not (self.expression):
+ raise ValueError(self.name + ": specify evaluation expression")
+ else:
+ raise ValueError(self.name + ": trigger behavior not supported")
+
+ def __repr__(self):
+ ts_cond_str = "TimeSeriesCondition: " + self.name
+ ts_cond_str += " statistics: " + str(self.keys)
+ ts_cond_str += " behavior: " + self.behavior.name
+ if self.behavior is TimeSeriesData.Behavior.bursty:
+ ts_cond_str += " rate_threshold: " + str(self.rate_threshold)
+ ts_cond_str += " window_sec: " + str(self.window_sec)
+ if self.behavior is TimeSeriesData.Behavior.evaluate_expression:
+ ts_cond_str += " expression: " + self.expression
+ if hasattr(self, "aggregation_op"):
+ ts_cond_str += " aggregation_op: " + self.aggregation_op.name
+ if self.trigger:
+ ts_cond_str += " trigger: " + str(self.trigger)
+ return ts_cond_str
+
+
+class RulesSpec:
+ def __init__(self, rules_path):
+ self.file_path = rules_path
+
+ def initialise_fields(self):
+ self.rules_dict = {}
+ self.conditions_dict = {}
+ self.suggestions_dict = {}
+
+ def perform_section_checks(self):
+ for rule in self.rules_dict.values():
+ rule.perform_checks()
+ for cond in self.conditions_dict.values():
+ cond.perform_checks()
+ for sugg in self.suggestions_dict.values():
+ sugg.perform_checks()
+
+ def load_rules_from_spec(self):
+ self.initialise_fields()
+ with open(self.file_path, "r") as db_rules:
+ curr_section = None
+ for line in db_rules:
+ line = IniParser.remove_trailing_comment(line)
+ if not line:
+ continue
+ element = IniParser.get_element(line)
+ if element is IniParser.Element.comment:
+ continue
+ elif element is not IniParser.Element.key_val:
+ curr_section = element # it's a new IniParser header
+ section_name = IniParser.get_section_name(line)
+ if element is IniParser.Element.rule:
+ new_rule = Rule(section_name)
+ self.rules_dict[section_name] = new_rule
+ elif element is IniParser.Element.cond:
+ new_cond = Condition(section_name)
+ self.conditions_dict[section_name] = new_cond
+ elif element is IniParser.Element.sugg:
+ new_suggestion = Suggestion(section_name)
+ self.suggestions_dict[section_name] = new_suggestion
+ elif element is IniParser.Element.key_val:
+ key, value = IniParser.get_key_value_pair(line)
+ if curr_section is IniParser.Element.rule:
+ new_rule.set_parameter(key, value)
+ elif curr_section is IniParser.Element.cond:
+ if key == "source":
+ if value == "LOG":
+ new_cond = LogCondition.create(new_cond)
+ elif value == "OPTIONS":
+ new_cond = OptionCondition.create(new_cond)
+ elif value == "TIME_SERIES":
+ new_cond = TimeSeriesCondition.create(new_cond)
+ else:
+ new_cond.set_parameter(key, value)
+ elif curr_section is IniParser.Element.sugg:
+ new_suggestion.set_parameter(key, value)
+
+ def get_rules_dict(self):
+ return self.rules_dict
+
+ def get_conditions_dict(self):
+ return self.conditions_dict
+
+ def get_suggestions_dict(self):
+ return self.suggestions_dict
+
+ def get_triggered_rules(self, data_sources, column_families):
+ self.trigger_conditions(data_sources)
+ triggered_rules = []
+ for rule in self.rules_dict.values():
+ if rule.is_triggered(self.conditions_dict, column_families):
+ triggered_rules.append(rule)
+ return triggered_rules
+
+ def trigger_conditions(self, data_sources):
+ for source_type in data_sources:
+ cond_subset = [
+ cond
+ for cond in self.conditions_dict.values()
+ if cond.get_data_source() is source_type
+ ]
+ if not cond_subset:
+ continue
+ for source in data_sources[source_type]:
+ source.check_and_trigger_conditions(cond_subset)
+
+ def print_rules(self, rules):
+ for rule in rules:
+ print("\nRule: " + rule.name)
+ for cond_name in rule.conditions:
+ print(repr(self.conditions_dict[cond_name]))
+ for sugg_name in rule.suggestions:
+ print(repr(self.suggestions_dict[sugg_name]))
+ if rule.trigger_entities:
+ print("scope: entities:")
+ print(rule.trigger_entities)
+ if rule.trigger_column_families:
+ print("scope: col_fam:")
+ print(rule.trigger_column_families)
diff --git a/src/rocksdb/tools/advisor/advisor/rule_parser_example.py b/src/rocksdb/tools/advisor/advisor/rule_parser_example.py
new file mode 100644
index 000000000..6c04ff2bf
--- /dev/null
+++ b/src/rocksdb/tools/advisor/advisor/rule_parser_example.py
@@ -0,0 +1,98 @@
+# Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+# This source code is licensed under both the GPLv2 (found in the
+# COPYING file in the root directory) and Apache 2.0 License
+# (found in the LICENSE.Apache file in the root directory).
+
+import argparse
+
+from advisor.db_log_parser import DatabaseLogs, DataSource
+from advisor.db_options_parser import DatabaseOptions
+from advisor.db_stats_fetcher import LogStatsParser, OdsStatsFetcher
+from advisor.rule_parser import RulesSpec
+
+
+def main(args):
+ # initialise the RulesSpec parser
+ rule_spec_parser = RulesSpec(args.rules_spec)
+ rule_spec_parser.load_rules_from_spec()
+ rule_spec_parser.perform_section_checks()
+ # initialize the DatabaseOptions object
+ db_options = DatabaseOptions(args.rocksdb_options)
+ # Create DatabaseLogs object
+ db_logs = DatabaseLogs(args.log_files_path_prefix, db_options.get_column_families())
+ # Create the Log STATS object
+ db_log_stats = LogStatsParser(
+ args.log_files_path_prefix, args.stats_dump_period_sec
+ )
+ data_sources = {
+ DataSource.Type.DB_OPTIONS: [db_options],
+ DataSource.Type.LOG: [db_logs],
+ DataSource.Type.TIME_SERIES: [db_log_stats],
+ }
+ if args.ods_client:
+ data_sources[DataSource.Type.TIME_SERIES].append(
+ OdsStatsFetcher(
+ args.ods_client,
+ args.ods_entity,
+ args.ods_tstart,
+ args.ods_tend,
+ args.ods_key_prefix,
+ )
+ )
+ triggered_rules = rule_spec_parser.get_triggered_rules(
+ data_sources, db_options.get_column_families()
+ )
+ rule_spec_parser.print_rules(triggered_rules)
+
+
+if __name__ == "__main__":
+ parser = argparse.ArgumentParser(
+ description="Use this script to get\
+ suggestions for improving Rocksdb performance."
+ )
+ parser.add_argument(
+ "--rules_spec",
+ required=True,
+ type=str,
+ help="path of the file containing the expert-specified Rules",
+ )
+ parser.add_argument(
+ "--rocksdb_options",
+ required=True,
+ type=str,
+ help="path of the starting Rocksdb OPTIONS file",
+ )
+ parser.add_argument(
+ "--log_files_path_prefix",
+ required=True,
+ type=str,
+ help="path prefix of the Rocksdb LOG files",
+ )
+ parser.add_argument(
+ "--stats_dump_period_sec",
+ required=True,
+ type=int,
+ help="the frequency (in seconds) at which STATISTICS are printed to "
+ + "the Rocksdb LOG file",
+ )
+ # ODS arguments
+ parser.add_argument("--ods_client", type=str, help="the ODS client binary")
+ parser.add_argument(
+ "--ods_entity",
+ type=str,
+ help="the servers for which the ODS stats need to be fetched",
+ )
+ parser.add_argument(
+ "--ods_key_prefix",
+ type=str,
+ help="the prefix that needs to be attached to the keys of time "
+ + "series to be fetched from ODS",
+ )
+ parser.add_argument(
+ "--ods_tstart", type=int, help="start time of timeseries to be fetched from ODS"
+ )
+ parser.add_argument(
+ "--ods_tend", type=int, help="end time of timeseries to be fetched from ODS"
+ )
+ args = parser.parse_args()
+ main(args)
diff --git a/src/rocksdb/tools/advisor/advisor/rules.ini b/src/rocksdb/tools/advisor/advisor/rules.ini
new file mode 100644
index 000000000..ec7a07e60
--- /dev/null
+++ b/src/rocksdb/tools/advisor/advisor/rules.ini
@@ -0,0 +1,214 @@
+# Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+# This source code is licensed under both the GPLv2 (found in the
+# COPYING file in the root directory) and Apache 2.0 License
+# (found in the LICENSE.Apache file in the root directory).
+#
+# FORMAT: very similar to the Rocksdb ini file in terms of syntax
+# (refer rocksdb/examples/rocksdb_option_file_example.ini)
+#
+# The Rules INI file is made up of multiple sections and each section is made
+# up of multiple key-value pairs. The recognized section types are:
+# Rule, Suggestion, Condition. Each section must have a name specified in ""
+# in the section header. This name acts as an identifier in that section
+# type's namespace. A section header looks like:
+# [<section_type> "<section_name_identifier>"]
+#
+# There should be at least one Rule section in the file with its corresponding
+# Condition and Suggestion sections. A Rule is triggered only when all of its
+# conditions are triggered. The order in which a Rule's conditions and
+# suggestions are specified has no significance.
+#
+# A Condition must be associated with a data source specified by the parameter
+# 'source' and this must be the first parameter specified for the Condition.
+# A condition can be associated with one or more Rules.
+#
+# A Suggestion is an advised change to a Rocksdb option to improve the
+# performance of the database in some way. Every suggestion can be a part of
+# one or more Rules.
+
+[Rule "stall-too-many-memtables"]
+suggestions=inc-bg-flush:inc-write-buffer
+conditions=stall-too-many-memtables
+
+[Condition "stall-too-many-memtables"]
+source=LOG
+regex=Stopping writes because we have \d+ immutable memtables \(waiting for flush\), max_write_buffer_number is set to \d+
+
+[Rule "stall-too-many-L0"]
+suggestions=inc-max-subcompactions:inc-max-bg-compactions:inc-write-buffer-size:dec-max-bytes-for-level-base:inc-l0-slowdown-writes-trigger
+conditions=stall-too-many-L0
+
+[Condition "stall-too-many-L0"]
+source=LOG
+regex=Stalling writes because we have \d+ level-0 files
+
+[Rule "stop-too-many-L0"]
+suggestions=inc-max-bg-compactions:inc-write-buffer-size:inc-l0-stop-writes-trigger
+conditions=stop-too-many-L0
+
+[Condition "stop-too-many-L0"]
+source=LOG
+regex=Stopping writes because we have \d+ level-0 files
+
+[Rule "stall-too-many-compaction-bytes"]
+suggestions=inc-max-bg-compactions:inc-write-buffer-size:inc-hard-pending-compaction-bytes-limit:inc-soft-pending-compaction-bytes-limit
+conditions=stall-too-many-compaction-bytes
+
+[Condition "stall-too-many-compaction-bytes"]
+source=LOG
+regex=Stalling writes because of estimated pending compaction bytes \d+
+
+[Suggestion "inc-bg-flush"]
+option=DBOptions.max_background_flushes
+action=increase
+suggested_values=2
+
+[Suggestion "inc-write-buffer"]
+option=CFOptions.max_write_buffer_number
+action=increase
+
+[Suggestion "inc-max-subcompactions"]
+option=DBOptions.max_subcompactions
+action=increase
+
+[Suggestion "inc-max-bg-compactions"]
+option=DBOptions.max_background_compactions
+action=increase
+suggested_values=2
+
+[Suggestion "inc-write-buffer-size"]
+option=CFOptions.write_buffer_size
+action=increase
+
+[Suggestion "dec-max-bytes-for-level-base"]
+option=CFOptions.max_bytes_for_level_base
+action=decrease
+
+[Suggestion "inc-l0-slowdown-writes-trigger"]
+option=CFOptions.level0_slowdown_writes_trigger
+action=increase
+
+[Suggestion "inc-l0-stop-writes-trigger"]
+option=CFOptions.level0_stop_writes_trigger
+action=increase
+
+[Suggestion "inc-hard-pending-compaction-bytes-limit"]
+option=CFOptions.hard_pending_compaction_bytes_limit
+action=increase
+
+[Suggestion "inc-soft-pending-compaction-bytes-limit"]
+option=CFOptions.soft_pending_compaction_bytes_limit
+action=increase
+
+[Rule "level0-level1-ratio"]
+conditions=level0-level1-ratio
+suggestions=inc-base-max-bytes
+
+[Condition "level0-level1-ratio"]
+source=OPTIONS
+options=CFOptions.level0_file_num_compaction_trigger:CFOptions.write_buffer_size:CFOptions.max_bytes_for_level_base
+evaluate=int(options[0])*int(options[1])-int(options[2])>=1 # should evaluate to a boolean, condition triggered if evaluates to true
+
+[Suggestion "inc-base-max-bytes"]
+option=CFOptions.max_bytes_for_level_base
+action=increase
+
+[Rules "tuning-iostat-burst"]
+conditions=large-db-get-p99
+suggestions=bytes-per-sync-non0:wal-bytes-per-sync-non0:set-rate-limiter
+#overlap_time_period=10m
+
+[Condition "write-burst"]
+source=TIME_SERIES
+keys=dyno.flash_write_bytes_per_sec
+behavior=bursty
+window_sec=300 # the smaller this window, the more sensitivity to changes in the time series, so the rate_threshold should be bigger; when it's 60, then same as diff(%)
+rate_threshold=20
+
+[Condition "large-p99-read-latency"]
+source=TIME_SERIES
+keys=[]rocksdb.read.block.get.micros.p99
+behavior=bursty
+window_sec=300
+rate_threshold=10
+
+[Condition "large-db-get-p99"]
+source=TIME_SERIES
+keys=[]rocksdb.db.get.micros.p50:[]rocksdb.db.get.micros.p99
+behavior=evaluate_expression
+evaluate=(keys[1]/keys[0])>5
+
+[Suggestion "bytes-per-sync-non0"]
+option=DBOptions.bytes_per_sync
+action=set
+suggested_values=1048576
+
+[Suggestion "wal-bytes-per-sync-non0"]
+option=DBOptions.wal_bytes_per_sync
+action=set
+suggested_values=1048576
+
+[Suggestion "set-rate-limiter"]
+option=rate_limiter_bytes_per_sec
+action=set
+suggested_values=1024000
+
+[Rule "bloom-filter-percent-useful"]
+conditions=bloom-filter-percent-useful
+suggestions=inc-bloom-bits-per-key
+
+[Condition "bloom-filter-percent-useful"]
+source=TIME_SERIES
+keys=[]rocksdb.bloom.filter.useful.count:[]rocksdb.bloom.filter.full.positive.count:[]rocksdb.bloom.filter.full.true.positive.count
+behavior=evaluate_expression
+evaluate=((keys[0]+keys[2])/(keys[0]+keys[1]))<0.9 # should evaluate to a boolean
+aggregation_op=latest
+
+[Rule "bloom-not-enabled"]
+conditions=bloom-not-enabled
+suggestions=inc-bloom-bits-per-key
+
+[Condition "bloom-not-enabled"]
+source=TIME_SERIES
+keys=[]rocksdb.bloom.filter.useful.count:[]rocksdb.bloom.filter.full.positive.count:[]rocksdb.bloom.filter.full.true.positive.count
+behavior=evaluate_expression
+evaluate=keys[0]+keys[1]+keys[2]==0
+aggregation_op=avg
+
+[Suggestion "inc-bloom-bits-per-key"]
+option=bloom_bits
+action=increase
+suggested_values=2
+
+[Rule "small-l0-files"]
+conditions=small-l0-files
+suggestions=dec-max-bytes-for-level-base:inc-write-buffer-size
+
+[Condition "small-l0-files"]
+source=OPTIONS
+options=CFOptions.max_bytes_for_level_base:CFOptions.level0_file_num_compaction_trigger:CFOptions.write_buffer_size
+evaluate=int(options[0])>(10*int(options[1])*int(options[2]))
+
+[Rule "decompress-time-long"]
+conditions=decompress-time-long
+suggestions=dec-block-size:inc-block-cache-size:faster-compression-type
+
+[Condition "decompress-time-long"]
+source=TIME_SERIES
+keys=block_decompress_time:block_read_time:block_checksum_time
+behavior=evaluate_expression
+evaluate=(keys[0]/(keys[0]+keys[1]+keys[2]))>0.3
+
+[Suggestion "dec-block-size"]
+option=TableOptions.BlockBasedTable.block_size
+action=decrease
+
+[Suggestion "inc-block-cache-size"]
+option=cache_size
+action=increase
+suggested_values=16000000
+
+[Suggestion "faster-compression-type"]
+option=CFOptions.compression
+action=set
+suggested_values=kLZ4Compression
diff --git a/src/rocksdb/tools/advisor/test/__init__.py b/src/rocksdb/tools/advisor/test/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/src/rocksdb/tools/advisor/test/__init__.py
diff --git a/src/rocksdb/tools/advisor/test/input_files/LOG-0 b/src/rocksdb/tools/advisor/test/input_files/LOG-0
new file mode 100644
index 000000000..3c9d51641
--- /dev/null
+++ b/src/rocksdb/tools/advisor/test/input_files/LOG-0
@@ -0,0 +1,30 @@
+2018/05/25-14:30:05.601692 7f82bd676200 RocksDB version: 5.14.0
+2018/05/25-14:30:07.626719 7f82ba72e700 (Original Log Time 2018/05/25-14:30:07.621966) [db/db_impl_compaction_flush.cc:1424] Calling FlushMemTableToOutputFile with column family [default], flush slots available 1, compaction slots available 1, flush slots scheduled 1, compaction slots scheduled 0
+2018/05/25-14:30:07.626725 7f82ba72e700 [db/flush_job.cc:301] [default] [JOB 3] Flushing memtable with next log file: 8
+2018/05/25-14:30:07.626738 7f82ba72e700 EVENT_LOG_v1 {"time_micros": 1527283807626732, "job": 3, "event": "flush_started", "num_memtables": 1, "num_entries": 28018, "num_deletes": 0, "memory_usage": 4065512, "flush_reason": "Write Buffer Full"}
+2018/05/25-14:30:07.626740 7f82ba72e700 [db/flush_job.cc:331] [default] [JOB 3] Level-0 flush table #10: started
+2018/05/25-14:30:07.764232 7f82b2f20700 [db/db_impl_write.cc:1373] [default] New memtable created with log file: #11. Immutable memtables: 1.
+2018/05/25-14:30:07.764240 7f82b2f20700 [WARN] [db/column_family.cc:743] [default] Stopping writes because we have 2 immutable memtables (waiting for flush), max_write_buffer_number is set to 2
+2018/05/23-11:53:12.800143 7f9f36b40700 [WARN] [db/column_family.cc:799] [default] Stalling writes because we have 4 level-0 files rate 39886
+2018/05/23-11:53:12.800143 7f9f36b40700 [WARN] [db/column_family.cc:799] [default] Stopping writes because we have 4 level-0 files rate 39886
+2018/05/25-14:30:09.398302 7f82ba72e700 EVENT_LOG_v1 {"time_micros": 1527283809398276, "cf_name": "default", "job": 3, "event": "table_file_creation", "file_number": 10, "file_size": 1890434, "table_properties": {"data_size": 1876749, "index_size": 23346, "filter_size": 0, "raw_key_size": 663120, "raw_average_key_size": 24, "raw_value_size": 2763000, "raw_average_value_size": 100, "num_data_blocks": 838, "num_entries": 27630, "filter_policy_name": "", "kDeletedKeys": "0", "kMergeOperands": "0"}}
+2018/05/25-14:30:09.398351 7f82ba72e700 [db/flush_job.cc:371] [default] [JOB 3] Level-0 flush table #10: 1890434 bytes OK
+2018/05/25-14:30:25.491635 7f82ba72e700 [db/flush_job.cc:331] [default] [JOB 10] Level-0 flush table #23: started
+2018/05/25-14:30:25.643618 7f82b2f20700 [db/db_impl_write.cc:1373] [default] New memtable created with log file: #24. Immutable memtables: 1.
+2018/05/25-14:30:25.643633 7f82b2f20700 [WARN] [db/column_family.cc:743] [default] Stopping writes because we have 2 immutable memtables (waiting for flush), max_write_buffer_number is set to 2
+2018/05/25-14:30:27.288181 7f82ba72e700 EVENT_LOG_v1 {"time_micros": 1527283827288158, "cf_name": "default", "job": 10, "event": "table_file_creation", "file_number": 23, "file_size": 1893200, "table_properties": {"data_size": 1879460, "index_size": 23340, "filter_size": 0, "raw_key_size": 663360, "raw_average_key_size": 24, "raw_value_size": 2764000, "raw_average_value_size": 100, "num_data_blocks": 838, "num_entries": 27640, "filter_policy_name": "", "kDeletedKeys": "0", "kMergeOperands": "0"}}
+2018/05/25-14:30:27.288210 7f82ba72e700 [db/flush_job.cc:371] [default] [JOB 10] Level-0 flush table #23: 1893200 bytes OK
+2018/05/25-14:30:27.289353 7f82ba72e700 [WARN] [db/column_family.cc:764] [default] Stalling writes because of estimated pending compaction bytes 14410584
+2018/05/25-14:30:27.289390 7f82ba72e700 (Original Log Time 2018/05/25-14:30:27.288829) [db/memtable_list.cc:377] [default] Level-0 commit table #23 started
+2018/05/25-14:30:27.289393 7f82ba72e700 (Original Log Time 2018/05/25-14:30:27.289332) [db/memtable_list.cc:409] [default] Level-0 commit table #23: memtable #1 done
+2018/05/25-14:34:21.047206 7f82ba72e700 EVENT_LOG_v1 {"time_micros": 1527284061047181, "cf_name": "default", "job": 44, "event": "table_file_creation", "file_number": 84, "file_size": 1890780, "table_properties": {"data_size": 1877100, "index_size": 23309, "filter_size": 0, "raw_key_size": 662808, "raw_average_key_size": 24, "raw_value_size": 2761700, "raw_average_value_size": 100, "num_data_blocks": 837, "num_entries": 27617, "filter_policy_name": "", "kDeletedKeys": "0", "kMergeOperands": "0"}}
+2018/05/25-14:34:21.047233 7f82ba72e700 [db/flush_job.cc:371] [default] [JOB 44] Level-0 flush table #84: 1890780 bytes OK
+2018/05/25-14:34:21.048017 7f82ba72e700 (Original Log Time 2018/05/25-14:34:21.048005) EVENT_LOG_v1 {"time_micros": 1527284061047997, "job": 44, "event": "flush_finished", "output_compression": "Snappy", "lsm_state": [2, 1, 0, 0, 0, 0, 0], "immutable_memtables": 1}
+2018/05/25-14:34:21.048592 7f82bd676200 [DEBUG] [db/db_impl_files.cc:261] [JOB 45] Delete /tmp/rocksdbtest-155919/dbbench/000084.sst type=2 #84 -- OK
+2018/05/25-14:34:21.048603 7f82bd676200 EVENT_LOG_v1 {"time_micros": 1527284061048600, "job": 45, "event": "table_file_deletion", "file_number": 84}
+2018/05/25-14:34:21.048981 7f82bd676200 [db/db_impl.cc:398] Shutdown complete
+2018/05/25-14:34:21.049000 7f82bd676200 [db/db_impl.cc:563] [col-fam-A] random log message for testing
+2018/05/25-14:34:21.049010 7f82bd676200 [db/db_impl.cc:234] [col-fam-B] log continuing on next line
+remaining part of the log
+2018/05/25-14:34:21.049020 7f82bd676200 [db/db_impl.cc:653] [col-fam-A] another random log message
+2018/05/25-14:34:21.049025 7f82bd676200 [db/db_impl.cc:331] [unknown] random log message no column family
diff --git a/src/rocksdb/tools/advisor/test/input_files/LOG-1 b/src/rocksdb/tools/advisor/test/input_files/LOG-1
new file mode 100644
index 000000000..b163f9a99
--- /dev/null
+++ b/src/rocksdb/tools/advisor/test/input_files/LOG-1
@@ -0,0 +1,25 @@
+2018/05/25-14:30:05.601692 7f82bd676200 RocksDB version: 5.14.0
+2018/05/25-14:30:07.626719 7f82ba72e700 (Original Log Time 2018/05/25-14:30:07.621966) [db/db_impl_compaction_flush.cc:1424] Calling FlushMemTableToOutputFile with column family [default], flush slots available 1, compaction slots available 1, flush slots scheduled 1, compaction slots scheduled 0
+2018/05/25-14:30:07.626725 7f82ba72e700 [db/flush_job.cc:301] [default] [JOB 3] Flushing memtable with next log file: 8
+2018/05/25-14:30:07.626738 7f82ba72e700 EVENT_LOG_v1 {"time_micros": 1527283807626732, "job": 3, "event": "flush_started", "num_memtables": 1, "num_entries": 28018, "num_deletes": 0, "memory_usage": 4065512, "flush_reason": "Write Buffer Full"}
+2018/05/25-14:30:07.626740 7f82ba72e700 [db/flush_job.cc:331] [default] [JOB 3] Level-0 flush table #10: started
+2018/05/25-14:30:07.764232 7f82b2f20700 [db/db_impl_write.cc:1373] [default] New memtable created with log file: #11. Immutable memtables: 1.
+2018/05/25-14:30:07.764240 7f82b2f20700 [WARN] [db/column_family.cc:743] [default] Stopping writes because we have 2 immutable memtables (waiting for flush), max_write_buffer_number is set to 2
+2018/05/23-11:53:12.800143 7f9f36b40700 [WARN] [db/column_family.cc:799] [default] Stalling writes because we have 4 level-0 files rate 39886
+2018/05/23-11:53:12.800143 7f9f36b40700 [WARN] [db/column_family.cc:799] [default] Stopping writes because we have 4 level-0 files rate 39886
+2018/05/25-14:30:09.398302 7f82ba72e700 EVENT_LOG_v1 {"time_micros": 1527283809398276, "cf_name": "default", "job": 3, "event": "table_file_creation", "file_number": 10, "file_size": 1890434, "table_properties": {"data_size": 1876749, "index_size": 23346, "filter_size": 0, "raw_key_size": 663120, "raw_average_key_size": 24, "raw_value_size": 2763000, "raw_average_value_size": 100, "num_data_blocks": 838, "num_entries": 27630, "filter_policy_name": "", "kDeletedKeys": "0", "kMergeOperands": "0"}}
+2018/05/25-14:30:09.398351 7f82ba72e700 [db/flush_job.cc:371] [default] [JOB 3] Level-0 flush table #10: 1890434 bytes OK
+2018/05/25-14:30:25.491635 7f82ba72e700 [db/flush_job.cc:331] [default] [JOB 10] Level-0 flush table #23: started
+2018/05/25-14:30:25.643618 7f82b2f20700 [db/db_impl_write.cc:1373] [default] New memtable created with log file: #24. Immutable memtables: 1.
+2018/05/25-14:30:25.643633 7f82b2f20700 [WARN] [db/column_family.cc:743] [default] Stopping writes because we have 2 immutable memtables (waiting for flush), max_write_buffer_number is set to 2
+2018/05/25-14:30:27.288181 7f82ba72e700 EVENT_LOG_v1 {"time_micros": 1527283827288158, "cf_name": "default", "job": 10, "event": "table_file_creation", "file_number": 23, "file_size": 1893200, "table_properties": {"data_size": 1879460, "index_size": 23340, "filter_size": 0, "raw_key_size": 663360, "raw_average_key_size": 24, "raw_value_size": 2764000, "raw_average_value_size": 100, "num_data_blocks": 838, "num_entries": 27640, "filter_policy_name": "", "kDeletedKeys": "0", "kMergeOperands": "0"}}
+2018/05/25-14:30:27.288210 7f82ba72e700 [db/flush_job.cc:371] [default] [JOB 10] Level-0 flush table #23: 1893200 bytes OK
+2018/05/25-14:30:27.289353 7f82ba72e700 [WARN] [db/column_family.cc:764] [default] Stopping writes because of estimated pending compaction bytes 14410584
+2018/05/25-14:30:27.289390 7f82ba72e700 (Original Log Time 2018/05/25-14:30:27.288829) [db/memtable_list.cc:377] [default] Level-0 commit table #23 started
+2018/05/25-14:30:27.289393 7f82ba72e700 (Original Log Time 2018/05/25-14:30:27.289332) [db/memtable_list.cc:409] [default] Level-0 commit table #23: memtable #1 done
+2018/05/25-14:34:21.047206 7f82ba72e700 EVENT_LOG_v1 {"time_micros": 1527284061047181, "cf_name": "default", "job": 44, "event": "table_file_creation", "file_number": 84, "file_size": 1890780, "table_properties": {"data_size": 1877100, "index_size": 23309, "filter_size": 0, "raw_key_size": 662808, "raw_average_key_size": 24, "raw_value_size": 2761700, "raw_average_value_size": 100, "num_data_blocks": 837, "num_entries": 27617, "filter_policy_name": "", "kDeletedKeys": "0", "kMergeOperands": "0"}}
+2018/05/25-14:34:21.047233 7f82ba72e700 [db/flush_job.cc:371] [default] [JOB 44] Level-0 flush table #84: 1890780 bytes OK
+2018/05/25-14:34:21.048017 7f82ba72e700 (Original Log Time 2018/05/25-14:34:21.048005) EVENT_LOG_v1 {"time_micros": 1527284061047997, "job": 44, "event": "flush_finished", "output_compression": "Snappy", "lsm_state": [2, 1, 0, 0, 0, 0, 0], "immutable_memtables": 1}
+2018/05/25-14:34:21.048592 7f82bd676200 [DEBUG] [db/db_impl_files.cc:261] [JOB 45] Delete /tmp/rocksdbtest-155919/dbbench/000084.sst type=2 #84 -- OK
+2018/05/25-14:34:21.048603 7f82bd676200 EVENT_LOG_v1 {"time_micros": 1527284061048600, "job": 45, "event": "table_file_deletion", "file_number": 84}
+2018/05/25-14:34:21.048981 7f82bd676200 [db/db_impl.cc:398] Shutdown complete
diff --git a/src/rocksdb/tools/advisor/test/input_files/OPTIONS-000005 b/src/rocksdb/tools/advisor/test/input_files/OPTIONS-000005
new file mode 100644
index 000000000..009edb04d
--- /dev/null
+++ b/src/rocksdb/tools/advisor/test/input_files/OPTIONS-000005
@@ -0,0 +1,49 @@
+# This is a RocksDB option file.
+#
+# For detailed file format spec, please refer to the example file
+# in examples/rocksdb_option_file_example.ini
+#
+
+[Version]
+ rocksdb_version=5.14.0
+ options_file_version=1.1
+
+[DBOptions]
+ manual_wal_flush=false
+ allow_ingest_behind=false
+ db_write_buffer_size=0
+ db_log_dir=
+ random_access_max_buffer_size=1048576
+
+[CFOptions "default"]
+ ttl=0
+ max_bytes_for_level_base=268435456
+ max_bytes_for_level_multiplier=10.000000
+ level0_file_num_compaction_trigger=4
+ level0_stop_writes_trigger=36
+ write_buffer_size=4194000
+ min_write_buffer_number_to_merge=1
+ num_levels=7
+ compaction_filter_factory=nullptr
+ compaction_style=kCompactionStyleLevel
+
+[TableOptions/BlockBasedTable "default"]
+ block_align=false
+ index_type=kBinarySearch
+
+[CFOptions "col_fam_A"]
+ttl=0
+max_bytes_for_level_base=268435456
+max_bytes_for_level_multiplier=10.000000
+level0_file_num_compaction_trigger=5
+level0_stop_writes_trigger=36
+write_buffer_size=1024000
+min_write_buffer_number_to_merge=1
+num_levels=5
+compaction_filter_factory=nullptr
+compaction_style=kCompactionStyleLevel
+
+[TableOptions/BlockBasedTable "col_fam_A"]
+block_align=true
+block_restart_interval=16
+index_type=kBinarySearch
diff --git a/src/rocksdb/tools/advisor/test/input_files/log_stats_parser_keys_ts b/src/rocksdb/tools/advisor/test/input_files/log_stats_parser_keys_ts
new file mode 100644
index 000000000..e8ade9e3e
--- /dev/null
+++ b/src/rocksdb/tools/advisor/test/input_files/log_stats_parser_keys_ts
@@ -0,0 +1,3 @@
+rocksdb.number.block.decompressed.count: 1530896335 88.0, 1530896361 788338.0, 1530896387 1539256.0, 1530896414 2255696.0, 1530896440 3009325.0, 1530896466 3767183.0, 1530896492 4529775.0, 1530896518 5297809.0, 1530896545 6033802.0, 1530896570 6794129.0
+rocksdb.db.get.micros.p50: 1530896335 295.5, 1530896361 16.561841, 1530896387 16.20677, 1530896414 16.31508, 1530896440 16.346602, 1530896466 16.284669, 1530896492 16.16005, 1530896518 16.069096, 1530896545 16.028746, 1530896570 15.9638
+rocksdb.manifest.file.sync.micros.p99: 1530896335 649.0, 1530896361 835.0, 1530896387 1435.0, 1530896414 9938.0, 1530896440 9938.0, 1530896466 9938.0, 1530896492 9938.0, 1530896518 1882.0, 1530896545 1837.0, 1530896570 1792.0
diff --git a/src/rocksdb/tools/advisor/test/input_files/rules_err1.ini b/src/rocksdb/tools/advisor/test/input_files/rules_err1.ini
new file mode 100644
index 000000000..23be55dde
--- /dev/null
+++ b/src/rocksdb/tools/advisor/test/input_files/rules_err1.ini
@@ -0,0 +1,56 @@
+[Rule "missing-suggestions"]
+suggestions=
+conditions=missing-source
+
+[Condition "normal-rule"]
+source=LOG
+regex=Stopping writes because we have \d+ immutable memtables \(waiting for flush\), max_write_buffer_number is set to \d+
+
+[Suggestion "inc-bg-flush"]
+option=DBOptions.max_background_flushes
+action=increase
+
+[Suggestion "inc-write-buffer"]
+option=CFOptions.max_write_buffer_number
+action=increase
+
+[Rule "missing-conditions"]
+conditions=
+suggestions=missing-description
+
+[Condition "missing-options"]
+source=OPTIONS
+options=
+evaluate=int(options[0])*int(options[1])-int(options[2])<(-251659456) # should evaluate to a boolean
+
+[Rule "missing-expression"]
+conditions=missing-expression
+suggestions=missing-description
+
+[Condition "missing-expression"]
+source=OPTIONS
+options=CFOptions.level0_file_num_compaction_trigger:CFOptions.write_buffer_size:CFOptions.max_bytes_for_level_base
+evaluate=
+
+[Suggestion "missing-description"]
+description=
+
+[Rule "stop-too-many-L0"]
+suggestions=inc-max-bg-compactions:missing-action:inc-l0-stop-writes-trigger
+conditions=missing-regex
+
+[Condition "missing-regex"]
+source=LOG
+regex=
+
+[Suggestion "missing-option"]
+option=
+action=increase
+
+[Suggestion "normal-suggestion"]
+option=CFOptions.write_buffer_size
+action=increase
+
+[Suggestion "inc-l0-stop-writes-trigger"]
+option=CFOptions.level0_stop_writes_trigger
+action=increase
diff --git a/src/rocksdb/tools/advisor/test/input_files/rules_err2.ini b/src/rocksdb/tools/advisor/test/input_files/rules_err2.ini
new file mode 100644
index 000000000..bce21dba9
--- /dev/null
+++ b/src/rocksdb/tools/advisor/test/input_files/rules_err2.ini
@@ -0,0 +1,15 @@
+[Rule "normal-rule"]
+suggestions=inc-bg-flush:inc-write-buffer
+conditions=missing-source
+
+[Condition "missing-source"]
+source=
+regex=Stopping writes because we have \d+ immutable memtables \(waiting for flush\), max_write_buffer_number is set to \d+
+
+[Suggestion "inc-bg-flush"]
+option=DBOptions.max_background_flushes
+action=increase
+
+[Suggestion "inc-write-buffer"]
+option=CFOptions.max_write_buffer_number
+action=increase
diff --git a/src/rocksdb/tools/advisor/test/input_files/rules_err3.ini b/src/rocksdb/tools/advisor/test/input_files/rules_err3.ini
new file mode 100644
index 000000000..73c06e469
--- /dev/null
+++ b/src/rocksdb/tools/advisor/test/input_files/rules_err3.ini
@@ -0,0 +1,15 @@
+[Rule "normal-rule"]
+suggestions=missing-action:inc-write-buffer
+conditions=missing-source
+
+[Condition "normal-condition"]
+source=LOG
+regex=Stopping writes because we have \d+ immutable memtables \(waiting for flush\), max_write_buffer_number is set to \d+
+
+[Suggestion "missing-action"]
+option=DBOptions.max_background_flushes
+action=
+
+[Suggestion "inc-write-buffer"]
+option=CFOptions.max_write_buffer_number
+action=increase
diff --git a/src/rocksdb/tools/advisor/test/input_files/rules_err4.ini b/src/rocksdb/tools/advisor/test/input_files/rules_err4.ini
new file mode 100644
index 000000000..4d4aa3c70
--- /dev/null
+++ b/src/rocksdb/tools/advisor/test/input_files/rules_err4.ini
@@ -0,0 +1,15 @@
+[Rule "normal-rule"]
+suggestions=inc-bg-flush
+conditions=missing-source
+
+[Condition "normal-condition"]
+source=LOG
+regex=Stopping writes because we have \d+ immutable memtables \(waiting for flush\), max_write_buffer_number is set to \d+
+
+[Suggestion "inc-bg-flush"]
+option=DBOptions.max_background_flushes
+action=increase
+
+[Suggestion] # missing section name
+option=CFOptions.max_write_buffer_number
+action=increase
diff --git a/src/rocksdb/tools/advisor/test/input_files/test_rules.ini b/src/rocksdb/tools/advisor/test/input_files/test_rules.ini
new file mode 100644
index 000000000..97b9374fc
--- /dev/null
+++ b/src/rocksdb/tools/advisor/test/input_files/test_rules.ini
@@ -0,0 +1,47 @@
+[Rule "single-condition-false"]
+suggestions=inc-bg-flush:inc-write-buffer
+conditions=log-4-false
+
+[Rule "multiple-conds-true"]
+suggestions=inc-write-buffer
+conditions=log-1-true:log-2-true:log-3-true
+
+[Rule "multiple-conds-one-false"]
+suggestions=inc-bg-flush
+conditions=log-1-true:log-4-false:log-3-true
+
+[Rule "multiple-conds-all-false"]
+suggestions=l0-l1-ratio-health-check
+conditions=log-4-false:options-1-false
+
+[Condition "log-1-true"]
+source=LOG
+regex=Stopping writes because we have \d+ immutable memtables \(waiting for flush\), max_write_buffer_number is set to \d+
+
+[Condition "log-2-true"]
+source=LOG
+regex=Stalling writes because we have \d+ level-0 files
+
+[Condition "log-3-true"]
+source=LOG
+regex=Stopping writes because we have \d+ level-0 files
+
+[Condition "log-4-false"]
+source=LOG
+regex=Stalling writes because of estimated pending compaction bytes \d+
+
+[Condition "options-1-false"]
+source=OPTIONS
+options=CFOptions.level0_file_num_compaction_trigger:CFOptions.write_buffer_size:DBOptions.random_access_max_buffer_size
+evaluate=int(options[0])*int(options[1])-int(options[2])<0 # should evaluate to a boolean
+
+[Suggestion "inc-bg-flush"]
+option=DBOptions.max_background_flushes
+action=increase
+
+[Suggestion "inc-write-buffer"]
+option=CFOptions.max_write_buffer_number
+action=increase
+
+[Suggestion "l0-l1-ratio-health-check"]
+description='modify options such that (level0_file_num_compaction_trigger * write_buffer_size - max_bytes_for_level_base < 5) is satisfied'
diff --git a/src/rocksdb/tools/advisor/test/input_files/triggered_rules.ini b/src/rocksdb/tools/advisor/test/input_files/triggered_rules.ini
new file mode 100644
index 000000000..83b96da2b
--- /dev/null
+++ b/src/rocksdb/tools/advisor/test/input_files/triggered_rules.ini
@@ -0,0 +1,83 @@
+[Rule "stall-too-many-memtables"]
+suggestions=inc-bg-flush:inc-write-buffer
+conditions=stall-too-many-memtables
+
+[Condition "stall-too-many-memtables"]
+source=LOG
+regex=Stopping writes because we have \d+ immutable memtables \(waiting for flush\), max_write_buffer_number is set to \d+
+
+[Rule "stall-too-many-L0"]
+suggestions=inc-max-subcompactions:inc-max-bg-compactions:inc-write-buffer-size:dec-max-bytes-for-level-base:inc-l0-slowdown-writes-trigger
+conditions=stall-too-many-L0
+
+[Condition "stall-too-many-L0"]
+source=LOG
+regex=Stalling writes because we have \d+ level-0 files
+
+[Rule "stop-too-many-L0"]
+suggestions=inc-max-bg-compactions:inc-write-buffer-size:inc-l0-stop-writes-trigger
+conditions=stop-too-many-L0
+
+[Condition "stop-too-many-L0"]
+source=LOG
+regex=Stopping writes because we have \d+ level-0 files
+
+[Rule "stall-too-many-compaction-bytes"]
+suggestions=inc-max-bg-compactions:inc-write-buffer-size:inc-hard-pending-compaction-bytes-limit:inc-soft-pending-compaction-bytes-limit
+conditions=stall-too-many-compaction-bytes
+
+[Condition "stall-too-many-compaction-bytes"]
+source=LOG
+regex=Stalling writes because of estimated pending compaction bytes \d+
+
+[Suggestion "inc-bg-flush"]
+option=DBOptions.max_background_flushes
+action=increase
+
+[Suggestion "inc-write-buffer"]
+option=CFOptions.max_write_buffer_number
+action=increase
+
+[Suggestion "inc-max-subcompactions"]
+option=DBOptions.max_subcompactions
+action=increase
+
+[Suggestion "inc-max-bg-compactions"]
+option=DBOptions.max_background_compactions
+action=increase
+
+[Suggestion "inc-write-buffer-size"]
+option=CFOptions.write_buffer_size
+action=increase
+
+[Suggestion "dec-max-bytes-for-level-base"]
+option=CFOptions.max_bytes_for_level_base
+action=decrease
+
+[Suggestion "inc-l0-slowdown-writes-trigger"]
+option=CFOptions.level0_slowdown_writes_trigger
+action=increase
+
+[Suggestion "inc-l0-stop-writes-trigger"]
+option=CFOptions.level0_stop_writes_trigger
+action=increase
+
+[Suggestion "inc-hard-pending-compaction-bytes-limit"]
+option=CFOptions.hard_pending_compaction_bytes_limit
+action=increase
+
+[Suggestion "inc-soft-pending-compaction-bytes-limit"]
+option=CFOptions.soft_pending_compaction_bytes_limit
+action=increase
+
+[Rule "level0-level1-ratio"]
+conditions=level0-level1-ratio
+suggestions=l0-l1-ratio-health-check
+
+[Condition "level0-level1-ratio"]
+source=OPTIONS
+options=CFOptions.level0_file_num_compaction_trigger:CFOptions.write_buffer_size:CFOptions.max_bytes_for_level_base
+evaluate=int(options[0])*int(options[1])-int(options[2])>=-268173312 # should evaluate to a boolean, condition triggered if evaluates to true
+
+[Suggestion "l0-l1-ratio-health-check"]
+description='modify options such that (level0_file_num_compaction_trigger * write_buffer_size - max_bytes_for_level_base < -268173312) is satisfied'
diff --git a/src/rocksdb/tools/advisor/test/test_db_bench_runner.py b/src/rocksdb/tools/advisor/test/test_db_bench_runner.py
new file mode 100644
index 000000000..57306c942
--- /dev/null
+++ b/src/rocksdb/tools/advisor/test/test_db_bench_runner.py
@@ -0,0 +1,141 @@
+# Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+# This source code is licensed under both the GPLv2 (found in the
+# COPYING file in the root directory) and Apache 2.0 License
+# (found in the LICENSE.Apache file in the root directory).
+
+import os
+import unittest
+
+from advisor.db_bench_runner import DBBenchRunner
+from advisor.db_log_parser import DataSource, NO_COL_FAMILY
+from advisor.db_options_parser import DatabaseOptions
+
+
+class TestDBBenchRunnerMethods(unittest.TestCase):
+ def setUp(self):
+ self.pos_args = [
+ "./../../db_bench",
+ "overwrite",
+ "use_existing_db=true",
+ "duration=10",
+ ]
+ self.bench_runner = DBBenchRunner(self.pos_args)
+ this_path = os.path.abspath(os.path.dirname(__file__))
+ options_path = os.path.join(this_path, "input_files/OPTIONS-000005")
+ self.db_options = DatabaseOptions(options_path)
+
+ def test_setup(self):
+ self.assertEqual(self.bench_runner.db_bench_binary, self.pos_args[0])
+ self.assertEqual(self.bench_runner.benchmark, self.pos_args[1])
+ self.assertSetEqual(
+ set(self.bench_runner.db_bench_args), set(self.pos_args[2:])
+ )
+
+ def test_get_info_log_file_name(self):
+ log_file_name = DBBenchRunner.get_info_log_file_name(None, "random_path")
+ self.assertEqual(log_file_name, "LOG")
+
+ log_file_name = DBBenchRunner.get_info_log_file_name(
+ "/dev/shm/", "/tmp/rocksdbtest-155919/dbbench/"
+ )
+ self.assertEqual(log_file_name, "tmp_rocksdbtest-155919_dbbench_LOG")
+
+ def test_get_opt_args_str(self):
+ misc_opt_dict = {"bloom_bits": 2, "empty_opt": None, "rate_limiter": 3}
+ optional_args_str = DBBenchRunner.get_opt_args_str(misc_opt_dict)
+ self.assertEqual(optional_args_str, " --bloom_bits=2 --rate_limiter=3")
+
+ def test_get_log_options(self):
+ db_path = "/tmp/rocksdb-155919/dbbench"
+ # when db_log_dir is present in the db_options
+ update_dict = {
+ "DBOptions.db_log_dir": {NO_COL_FAMILY: "/dev/shm"},
+ "DBOptions.stats_dump_period_sec": {NO_COL_FAMILY: "20"},
+ }
+ self.db_options.update_options(update_dict)
+ log_file_prefix, stats_freq = self.bench_runner.get_log_options(
+ self.db_options, db_path
+ )
+ self.assertEqual(log_file_prefix, "/dev/shm/tmp_rocksdb-155919_dbbench_LOG")
+ self.assertEqual(stats_freq, 20)
+
+ update_dict = {
+ "DBOptions.db_log_dir": {NO_COL_FAMILY: None},
+ "DBOptions.stats_dump_period_sec": {NO_COL_FAMILY: "30"},
+ }
+ self.db_options.update_options(update_dict)
+ log_file_prefix, stats_freq = self.bench_runner.get_log_options(
+ self.db_options, db_path
+ )
+ self.assertEqual(log_file_prefix, "/tmp/rocksdb-155919/dbbench/LOG")
+ self.assertEqual(stats_freq, 30)
+
+ def test_build_experiment_command(self):
+ # add some misc_options to db_options
+ update_dict = {
+ "bloom_bits": {NO_COL_FAMILY: 2},
+ "rate_limiter_bytes_per_sec": {NO_COL_FAMILY: 128000000},
+ }
+ self.db_options.update_options(update_dict)
+ db_path = "/dev/shm"
+ experiment_command = self.bench_runner._build_experiment_command(
+ self.db_options, db_path
+ )
+ opt_args_str = DBBenchRunner.get_opt_args_str(
+ self.db_options.get_misc_options()
+ )
+ opt_args_str += " --options_file=" + self.db_options.generate_options_config(
+ "12345"
+ )
+ for arg in self.pos_args[2:]:
+ opt_args_str += " --" + arg
+ expected_command = (
+ self.pos_args[0]
+ + " --benchmarks="
+ + self.pos_args[1]
+ + " --statistics --perf_level=3 --db="
+ + db_path
+ + opt_args_str
+ )
+ self.assertEqual(experiment_command, expected_command)
+
+
+class TestDBBenchRunner(unittest.TestCase):
+ def setUp(self):
+ # Note: the db_bench binary should be present in the rocksdb/ directory
+ self.pos_args = [
+ "./../../db_bench",
+ "overwrite",
+ "use_existing_db=true",
+ "duration=20",
+ ]
+ self.bench_runner = DBBenchRunner(self.pos_args)
+ this_path = os.path.abspath(os.path.dirname(__file__))
+ options_path = os.path.join(this_path, "input_files/OPTIONS-000005")
+ self.db_options = DatabaseOptions(options_path)
+
+ def test_experiment_output(self):
+ update_dict = {"bloom_bits": {NO_COL_FAMILY: 2}}
+ self.db_options.update_options(update_dict)
+ db_path = "/dev/shm"
+ data_sources, throughput = self.bench_runner.run_experiment(
+ self.db_options, db_path
+ )
+ self.assertEqual(
+ data_sources[DataSource.Type.DB_OPTIONS][0].type, DataSource.Type.DB_OPTIONS
+ )
+ self.assertEqual(data_sources[DataSource.Type.LOG][0].type, DataSource.Type.LOG)
+ self.assertEqual(len(data_sources[DataSource.Type.TIME_SERIES]), 2)
+ self.assertEqual(
+ data_sources[DataSource.Type.TIME_SERIES][0].type,
+ DataSource.Type.TIME_SERIES,
+ )
+ self.assertEqual(
+ data_sources[DataSource.Type.TIME_SERIES][1].type,
+ DataSource.Type.TIME_SERIES,
+ )
+ self.assertEqual(data_sources[DataSource.Type.TIME_SERIES][1].stats_freq_sec, 0)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/src/rocksdb/tools/advisor/test/test_db_log_parser.py b/src/rocksdb/tools/advisor/test/test_db_log_parser.py
new file mode 100644
index 000000000..6862691c1
--- /dev/null
+++ b/src/rocksdb/tools/advisor/test/test_db_log_parser.py
@@ -0,0 +1,96 @@
+# Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+# This source code is licensed under both the GPLv2 (found in the
+# COPYING file in the root directory) and Apache 2.0 License
+# (found in the LICENSE.Apache file in the root directory).
+
+import os
+import unittest
+
+from advisor.db_log_parser import DatabaseLogs, Log, NO_COL_FAMILY
+from advisor.rule_parser import Condition, LogCondition
+
+
+class TestLog(unittest.TestCase):
+ def setUp(self):
+ self.column_families = ["default", "col_fam_A"]
+
+ def test_get_column_family(self):
+ test_log = (
+ "2018/05/25-14:34:21.047233 7f82ba72e700 [db/flush_job.cc:371] "
+ + "[col_fam_A] [JOB 44] Level-0 flush table #84: 1890780 bytes OK"
+ )
+ db_log = Log(test_log, self.column_families)
+ self.assertEqual("col_fam_A", db_log.get_column_family())
+
+ test_log = (
+ "2018/05/25-14:34:21.047233 7f82ba72e700 [db/flush_job.cc:371] "
+ + "[JOB 44] Level-0 flush table #84: 1890780 bytes OK"
+ )
+ db_log = Log(test_log, self.column_families)
+ db_log.append_message("[default] some remaining part of log")
+ self.assertEqual(NO_COL_FAMILY, db_log.get_column_family())
+
+ def test_get_methods(self):
+ hr_time = "2018/05/25-14:30:25.491635"
+ context = "7f82ba72e700"
+ message = (
+ "[db/flush_job.cc:331] [default] [JOB 10] Level-0 flush table "
+ + "#23: started"
+ )
+ test_log = hr_time + " " + context + " " + message
+ db_log = Log(test_log, self.column_families)
+ self.assertEqual(db_log.get_message(), message)
+ remaining_message = "[col_fam_A] some more logs"
+ db_log.append_message(remaining_message)
+ self.assertEqual(db_log.get_human_readable_time(), "2018/05/25-14:30:25.491635")
+ self.assertEqual(db_log.get_context(), "7f82ba72e700")
+ self.assertEqual(db_log.get_timestamp(), 1527258625)
+ self.assertEqual(db_log.get_message(), str(message + "\n" + remaining_message))
+
+ def test_is_new_log(self):
+ new_log = "2018/05/25-14:34:21.047233 context random new log"
+ remaining_log = "2018/05/25 not really a new log"
+ self.assertTrue(Log.is_new_log(new_log))
+ self.assertFalse(Log.is_new_log(remaining_log))
+
+
+class TestDatabaseLogs(unittest.TestCase):
+ def test_check_and_trigger_conditions(self):
+ this_path = os.path.abspath(os.path.dirname(__file__))
+ logs_path_prefix = os.path.join(this_path, "input_files/LOG-0")
+ column_families = ["default", "col-fam-A", "col-fam-B"]
+ db_logs = DatabaseLogs(logs_path_prefix, column_families)
+ # matches, has 2 col_fams
+ condition1 = LogCondition.create(Condition("cond-A"))
+ condition1.set_parameter("regex", "random log message")
+ # matches, multiple lines message
+ condition2 = LogCondition.create(Condition("cond-B"))
+ condition2.set_parameter("regex", "continuing on next line")
+ # does not match
+ condition3 = LogCondition.create(Condition("cond-C"))
+ condition3.set_parameter("regex", "this should match no log")
+ db_logs.check_and_trigger_conditions([condition1, condition2, condition3])
+ cond1_trigger = condition1.get_trigger()
+ self.assertEqual(2, len(cond1_trigger.keys()))
+ self.assertSetEqual({"col-fam-A", NO_COL_FAMILY}, set(cond1_trigger.keys()))
+ self.assertEqual(2, len(cond1_trigger["col-fam-A"]))
+ messages = [
+ "[db/db_impl.cc:563] [col-fam-A] random log message for testing",
+ "[db/db_impl.cc:653] [col-fam-A] another random log message",
+ ]
+ self.assertIn(cond1_trigger["col-fam-A"][0].get_message(), messages)
+ self.assertIn(cond1_trigger["col-fam-A"][1].get_message(), messages)
+ self.assertEqual(1, len(cond1_trigger[NO_COL_FAMILY]))
+ self.assertEqual(
+ cond1_trigger[NO_COL_FAMILY][0].get_message(),
+ "[db/db_impl.cc:331] [unknown] random log message no column family",
+ )
+ cond2_trigger = condition2.get_trigger()
+ self.assertEqual(["col-fam-B"], list(cond2_trigger.keys()))
+ self.assertEqual(1, len(cond2_trigger["col-fam-B"]))
+ self.assertEqual(
+ cond2_trigger["col-fam-B"][0].get_message(),
+ "[db/db_impl.cc:234] [col-fam-B] log continuing on next line\n"
+ + "remaining part of the log",
+ )
+ self.assertIsNone(condition3.get_trigger())
diff --git a/src/rocksdb/tools/advisor/test/test_db_options_parser.py b/src/rocksdb/tools/advisor/test/test_db_options_parser.py
new file mode 100644
index 000000000..cdeebaefa
--- /dev/null
+++ b/src/rocksdb/tools/advisor/test/test_db_options_parser.py
@@ -0,0 +1,214 @@
+# Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+# This source code is licensed under both the GPLv2 (found in the
+# COPYING file in the root directory) and Apache 2.0 License
+# (found in the LICENSE.Apache file in the root directory).
+
+import os
+import unittest
+
+from advisor.db_log_parser import NO_COL_FAMILY
+from advisor.db_options_parser import DatabaseOptions
+from advisor.rule_parser import Condition, OptionCondition
+
+
+class TestDatabaseOptions(unittest.TestCase):
+ def setUp(self):
+ self.this_path = os.path.abspath(os.path.dirname(__file__))
+ self.og_options = os.path.join(self.this_path, "input_files/OPTIONS-000005")
+ misc_options = ["bloom_bits = 4", "rate_limiter_bytes_per_sec = 1024000"]
+ # create the options object
+ self.db_options = DatabaseOptions(self.og_options, misc_options)
+ # perform clean-up before running tests
+ self.generated_options = os.path.join(
+ self.this_path, "../temp/OPTIONS_testing.tmp"
+ )
+ if os.path.isfile(self.generated_options):
+ os.remove(self.generated_options)
+
+ def test_get_options_diff(self):
+ old_opt = {
+ "DBOptions.stats_dump_freq_sec": {NO_COL_FAMILY: "20"},
+ "CFOptions.write_buffer_size": {
+ "default": "1024000",
+ "col_fam_A": "128000",
+ "col_fam_B": "128000000",
+ },
+ "DBOptions.use_fsync": {NO_COL_FAMILY: "true"},
+ "DBOptions.max_log_file_size": {NO_COL_FAMILY: "128000000"},
+ }
+ new_opt = {
+ "bloom_bits": {NO_COL_FAMILY: "4"},
+ "CFOptions.write_buffer_size": {
+ "default": "128000000",
+ "col_fam_A": "128000",
+ "col_fam_C": "128000000",
+ },
+ "DBOptions.use_fsync": {NO_COL_FAMILY: "true"},
+ "DBOptions.max_log_file_size": {NO_COL_FAMILY: "0"},
+ }
+ diff = DatabaseOptions.get_options_diff(old_opt, new_opt)
+
+ expected_diff = {
+ "DBOptions.stats_dump_freq_sec": {NO_COL_FAMILY: ("20", None)},
+ "bloom_bits": {NO_COL_FAMILY: (None, "4")},
+ "CFOptions.write_buffer_size": {
+ "default": ("1024000", "128000000"),
+ "col_fam_B": ("128000000", None),
+ "col_fam_C": (None, "128000000"),
+ },
+ "DBOptions.max_log_file_size": {NO_COL_FAMILY: ("128000000", "0")},
+ }
+ self.assertDictEqual(diff, expected_diff)
+
+ def test_is_misc_option(self):
+ self.assertTrue(DatabaseOptions.is_misc_option("bloom_bits"))
+ self.assertFalse(
+ DatabaseOptions.is_misc_option("DBOptions.stats_dump_freq_sec")
+ )
+
+ def test_set_up(self):
+ options = self.db_options.get_all_options()
+ self.assertEqual(22, len(options.keys()))
+ expected_misc_options = {
+ "bloom_bits": "4",
+ "rate_limiter_bytes_per_sec": "1024000",
+ }
+ self.assertDictEqual(expected_misc_options, self.db_options.get_misc_options())
+ self.assertListEqual(
+ ["default", "col_fam_A"], self.db_options.get_column_families()
+ )
+
+ def test_get_options(self):
+ opt_to_get = [
+ "DBOptions.manual_wal_flush",
+ "DBOptions.db_write_buffer_size",
+ "bloom_bits",
+ "CFOptions.compaction_filter_factory",
+ "CFOptions.num_levels",
+ "rate_limiter_bytes_per_sec",
+ "TableOptions.BlockBasedTable.block_align",
+ "random_option",
+ ]
+ options = self.db_options.get_options(opt_to_get)
+ expected_options = {
+ "DBOptions.manual_wal_flush": {NO_COL_FAMILY: "false"},
+ "DBOptions.db_write_buffer_size": {NO_COL_FAMILY: "0"},
+ "bloom_bits": {NO_COL_FAMILY: "4"},
+ "CFOptions.compaction_filter_factory": {
+ "default": "nullptr",
+ "col_fam_A": "nullptr",
+ },
+ "CFOptions.num_levels": {"default": "7", "col_fam_A": "5"},
+ "rate_limiter_bytes_per_sec": {NO_COL_FAMILY: "1024000"},
+ "TableOptions.BlockBasedTable.block_align": {
+ "default": "false",
+ "col_fam_A": "true",
+ },
+ }
+ self.assertDictEqual(expected_options, options)
+
+ def test_update_options(self):
+ # add new, update old, set old
+ # before updating
+ expected_old_opts = {
+ "DBOptions.db_log_dir": {NO_COL_FAMILY: None},
+ "DBOptions.manual_wal_flush": {NO_COL_FAMILY: "false"},
+ "bloom_bits": {NO_COL_FAMILY: "4"},
+ "CFOptions.num_levels": {"default": "7", "col_fam_A": "5"},
+ "TableOptions.BlockBasedTable.block_restart_interval": {"col_fam_A": "16"},
+ }
+ get_opts = list(expected_old_opts.keys())
+ options = self.db_options.get_options(get_opts)
+ self.assertEqual(expected_old_opts, options)
+ # after updating options
+ update_opts = {
+ "DBOptions.db_log_dir": {NO_COL_FAMILY: "/dev/shm"},
+ "DBOptions.manual_wal_flush": {NO_COL_FAMILY: "true"},
+ "bloom_bits": {NO_COL_FAMILY: "2"},
+ "CFOptions.num_levels": {"col_fam_A": "7"},
+ "TableOptions.BlockBasedTable.block_restart_interval": {"default": "32"},
+ "random_misc_option": {NO_COL_FAMILY: "something"},
+ }
+ self.db_options.update_options(update_opts)
+ update_opts["CFOptions.num_levels"]["default"] = "7"
+ update_opts["TableOptions.BlockBasedTable.block_restart_interval"] = {
+ "default": "32",
+ "col_fam_A": "16",
+ }
+ get_opts.append("random_misc_option")
+ options = self.db_options.get_options(get_opts)
+ self.assertDictEqual(update_opts, options)
+ expected_misc_options = {
+ "bloom_bits": "2",
+ "rate_limiter_bytes_per_sec": "1024000",
+ "random_misc_option": "something",
+ }
+ self.assertDictEqual(expected_misc_options, self.db_options.get_misc_options())
+
+ def test_generate_options_config(self):
+ # make sure file does not exist from before
+ self.assertFalse(os.path.isfile(self.generated_options))
+ self.db_options.generate_options_config("testing")
+ self.assertTrue(os.path.isfile(self.generated_options))
+
+ def test_check_and_trigger_conditions(self):
+ # options only from CFOptions
+ # setup the OptionCondition objects to check and trigger
+ update_dict = {
+ "CFOptions.level0_file_num_compaction_trigger": {"col_fam_A": "4"},
+ "CFOptions.max_bytes_for_level_base": {"col_fam_A": "10"},
+ }
+ self.db_options.update_options(update_dict)
+ cond1 = Condition("opt-cond-1")
+ cond1 = OptionCondition.create(cond1)
+ cond1.set_parameter(
+ "options",
+ [
+ "CFOptions.level0_file_num_compaction_trigger",
+ "TableOptions.BlockBasedTable.block_restart_interval",
+ "CFOptions.max_bytes_for_level_base",
+ ],
+ )
+ cond1.set_parameter(
+ "evaluate", "int(options[0])*int(options[1])-int(options[2])>=0"
+ )
+ # only DBOptions
+ cond2 = Condition("opt-cond-2")
+ cond2 = OptionCondition.create(cond2)
+ cond2.set_parameter(
+ "options",
+ [
+ "DBOptions.db_write_buffer_size",
+ "bloom_bits",
+ "rate_limiter_bytes_per_sec",
+ ],
+ )
+ cond2.set_parameter(
+ "evaluate", "(int(options[2]) * int(options[1]) * int(options[0]))==0"
+ )
+ # mix of CFOptions and DBOptions
+ cond3 = Condition("opt-cond-3")
+ cond3 = OptionCondition.create(cond3)
+ cond3.set_parameter(
+ "options",
+ [
+ "DBOptions.db_write_buffer_size", # 0
+ "CFOptions.num_levels", # 5, 7
+ "bloom_bits", # 4
+ ],
+ )
+ cond3.set_parameter(
+ "evaluate", "int(options[2])*int(options[0])+int(options[1])>6"
+ )
+ self.db_options.check_and_trigger_conditions([cond1, cond2, cond3])
+
+ cond1_trigger = {"col_fam_A": ["4", "16", "10"]}
+ self.assertDictEqual(cond1_trigger, cond1.get_trigger())
+ cond2_trigger = {NO_COL_FAMILY: ["0", "4", "1024000"]}
+ self.assertDictEqual(cond2_trigger, cond2.get_trigger())
+ cond3_trigger = {"default": ["0", "7", "4"]}
+ self.assertDictEqual(cond3_trigger, cond3.get_trigger())
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/src/rocksdb/tools/advisor/test/test_db_stats_fetcher.py b/src/rocksdb/tools/advisor/test/test_db_stats_fetcher.py
new file mode 100644
index 000000000..e2c29ab74
--- /dev/null
+++ b/src/rocksdb/tools/advisor/test/test_db_stats_fetcher.py
@@ -0,0 +1,121 @@
+# Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+# This source code is licensed under both the GPLv2 (found in the
+# COPYING file in the root directory) and Apache 2.0 License
+# (found in the LICENSE.Apache file in the root directory).
+
+import os
+import time
+import unittest
+from unittest.mock import MagicMock
+
+from advisor.db_stats_fetcher import DatabasePerfContext, LogStatsParser
+from advisor.db_timeseries_parser import NO_ENTITY
+from advisor.rule_parser import Condition, TimeSeriesCondition
+
+
+class TestLogStatsParser(unittest.TestCase):
+ def setUp(self):
+ this_path = os.path.abspath(os.path.dirname(__file__))
+ stats_file = os.path.join(this_path, "input_files/log_stats_parser_keys_ts")
+ # populate the keys_ts dictionary of LogStatsParser
+ self.stats_dict = {NO_ENTITY: {}}
+ with open(stats_file, "r") as fp:
+ for line in fp:
+ stat_name = line.split(":")[0].strip()
+ self.stats_dict[NO_ENTITY][stat_name] = {}
+ token_list = line.split(":")[1].strip().split(",")
+ for token in token_list:
+ timestamp = int(token.split()[0])
+ value = float(token.split()[1])
+ self.stats_dict[NO_ENTITY][stat_name][timestamp] = value
+ self.log_stats_parser = LogStatsParser("dummy_log_file", 20)
+ self.log_stats_parser.keys_ts = self.stats_dict
+
+ def test_check_and_trigger_conditions_bursty(self):
+ # mock fetch_timeseries() because 'keys_ts' has been pre-populated
+ self.log_stats_parser.fetch_timeseries = MagicMock()
+ # condition: bursty
+ cond1 = Condition("cond-1")
+ cond1 = TimeSeriesCondition.create(cond1)
+ cond1.set_parameter("keys", "rocksdb.db.get.micros.p50")
+ cond1.set_parameter("behavior", "bursty")
+ cond1.set_parameter("window_sec", 40)
+ cond1.set_parameter("rate_threshold", 0)
+ self.log_stats_parser.check_and_trigger_conditions([cond1])
+ expected_cond_trigger = {NO_ENTITY: {1530896440: 0.9767546362322214}}
+ self.assertDictEqual(expected_cond_trigger, cond1.get_trigger())
+ # ensure that fetch_timeseries() was called once
+ self.log_stats_parser.fetch_timeseries.assert_called_once()
+
+ def test_check_and_trigger_conditions_eval_agg(self):
+ # mock fetch_timeseries() because 'keys_ts' has been pre-populated
+ self.log_stats_parser.fetch_timeseries = MagicMock()
+ # condition: evaluate_expression
+ cond1 = Condition("cond-1")
+ cond1 = TimeSeriesCondition.create(cond1)
+ cond1.set_parameter("keys", "rocksdb.db.get.micros.p50")
+ cond1.set_parameter("behavior", "evaluate_expression")
+ keys = ["rocksdb.manifest.file.sync.micros.p99", "rocksdb.db.get.micros.p50"]
+ cond1.set_parameter("keys", keys)
+ cond1.set_parameter("aggregation_op", "latest")
+ # condition evaluates to FALSE
+ cond1.set_parameter("evaluate", "keys[0]-(keys[1]*100)>200")
+ self.log_stats_parser.check_and_trigger_conditions([cond1])
+ expected_cond_trigger = {NO_ENTITY: [1792.0, 15.9638]}
+ self.assertIsNone(cond1.get_trigger())
+ # condition evaluates to TRUE
+ cond1.set_parameter("evaluate", "keys[0]-(keys[1]*100)<200")
+ self.log_stats_parser.check_and_trigger_conditions([cond1])
+ expected_cond_trigger = {NO_ENTITY: [1792.0, 15.9638]}
+ self.assertDictEqual(expected_cond_trigger, cond1.get_trigger())
+ # ensure that fetch_timeseries() was called
+ self.log_stats_parser.fetch_timeseries.assert_called()
+
+ def test_check_and_trigger_conditions_eval(self):
+ # mock fetch_timeseries() because 'keys_ts' has been pre-populated
+ self.log_stats_parser.fetch_timeseries = MagicMock()
+ # condition: evaluate_expression
+ cond1 = Condition("cond-1")
+ cond1 = TimeSeriesCondition.create(cond1)
+ cond1.set_parameter("keys", "rocksdb.db.get.micros.p50")
+ cond1.set_parameter("behavior", "evaluate_expression")
+ keys = ["rocksdb.manifest.file.sync.micros.p99", "rocksdb.db.get.micros.p50"]
+ cond1.set_parameter("keys", keys)
+ cond1.set_parameter("evaluate", "keys[0]-(keys[1]*100)>500")
+ self.log_stats_parser.check_and_trigger_conditions([cond1])
+ expected_trigger = {
+ NO_ENTITY: {
+ 1530896414: [9938.0, 16.31508],
+ 1530896440: [9938.0, 16.346602],
+ 1530896466: [9938.0, 16.284669],
+ 1530896492: [9938.0, 16.16005],
+ }
+ }
+ self.assertDictEqual(expected_trigger, cond1.get_trigger())
+ self.log_stats_parser.fetch_timeseries.assert_called_once()
+
+
+class TestDatabasePerfContext(unittest.TestCase):
+ def test_unaccumulate_metrics(self):
+ perf_dict = {
+ "user_key_comparison_count": 675903942,
+ "block_cache_hit_count": 830086,
+ }
+ timestamp = int(time.time())
+ perf_ts = {}
+ for key in perf_dict:
+ perf_ts[key] = {}
+ start_val = perf_dict[key]
+ for ix in range(5):
+ perf_ts[key][timestamp + (ix * 10)] = start_val + (2 * ix * ix)
+ db_perf_context = DatabasePerfContext(perf_ts, 10, True)
+ timestamps = [timestamp + (ix * 10) for ix in range(1, 5, 1)]
+ values = [val for val in range(2, 15, 4)]
+ inner_dict = {timestamps[ix]: values[ix] for ix in range(4)}
+ expected_keys_ts = {
+ NO_ENTITY: {
+ "user_key_comparison_count": inner_dict,
+ "block_cache_hit_count": inner_dict,
+ }
+ }
+ self.assertDictEqual(expected_keys_ts, db_perf_context.keys_ts)
diff --git a/src/rocksdb/tools/advisor/test/test_rule_parser.py b/src/rocksdb/tools/advisor/test/test_rule_parser.py
new file mode 100644
index 000000000..4ea4ca159
--- /dev/null
+++ b/src/rocksdb/tools/advisor/test/test_rule_parser.py
@@ -0,0 +1,226 @@
+# Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+# This source code is licensed under both the GPLv2 (found in the
+# COPYING file in the root directory) and Apache 2.0 License
+# (found in the LICENSE.Apache file in the root directory).
+
+import os
+import unittest
+
+from advisor.db_log_parser import DatabaseLogs, DataSource
+from advisor.db_options_parser import DatabaseOptions
+from advisor.rule_parser import RulesSpec
+
+RuleToSuggestions = {
+ "stall-too-many-memtables": ["inc-bg-flush", "inc-write-buffer"],
+ "stall-too-many-L0": [
+ "inc-max-subcompactions",
+ "inc-max-bg-compactions",
+ "inc-write-buffer-size",
+ "dec-max-bytes-for-level-base",
+ "inc-l0-slowdown-writes-trigger",
+ ],
+ "stop-too-many-L0": [
+ "inc-max-bg-compactions",
+ "inc-write-buffer-size",
+ "inc-l0-stop-writes-trigger",
+ ],
+ "stall-too-many-compaction-bytes": [
+ "inc-max-bg-compactions",
+ "inc-write-buffer-size",
+ "inc-hard-pending-compaction-bytes-limit",
+ "inc-soft-pending-compaction-bytes-limit",
+ ],
+ "level0-level1-ratio": ["l0-l1-ratio-health-check"],
+}
+
+
+class TestAllRulesTriggered(unittest.TestCase):
+ def setUp(self):
+ # load the Rules
+ this_path = os.path.abspath(os.path.dirname(__file__))
+ ini_path = os.path.join(this_path, "input_files/triggered_rules.ini")
+ self.db_rules = RulesSpec(ini_path)
+ self.db_rules.load_rules_from_spec()
+ self.db_rules.perform_section_checks()
+ # load the data sources: LOG and OPTIONS
+ log_path = os.path.join(this_path, "input_files/LOG-0")
+ options_path = os.path.join(this_path, "input_files/OPTIONS-000005")
+ db_options_parser = DatabaseOptions(options_path)
+ self.column_families = db_options_parser.get_column_families()
+ db_logs_parser = DatabaseLogs(log_path, self.column_families)
+ self.data_sources = {
+ DataSource.Type.DB_OPTIONS: [db_options_parser],
+ DataSource.Type.LOG: [db_logs_parser],
+ }
+
+ def test_triggered_conditions(self):
+ conditions_dict = self.db_rules.get_conditions_dict()
+ rules_dict = self.db_rules.get_rules_dict()
+ # Make sure none of the conditions is triggered beforehand
+ for cond in conditions_dict.values():
+ self.assertFalse(cond.is_triggered(), repr(cond))
+ for rule in rules_dict.values():
+ self.assertFalse(
+ rule.is_triggered(conditions_dict, self.column_families), repr(rule)
+ )
+
+ # # Trigger the conditions as per the data sources.
+ # trigger_conditions(, conditions_dict)
+
+ # Get the set of rules that have been triggered
+ triggered_rules = self.db_rules.get_triggered_rules(
+ self.data_sources, self.column_families
+ )
+
+ # Make sure each condition and rule is triggered
+ for cond in conditions_dict.values():
+ if cond.get_data_source() is DataSource.Type.TIME_SERIES:
+ continue
+ self.assertTrue(cond.is_triggered(), repr(cond))
+
+ for rule in rules_dict.values():
+ self.assertIn(rule, triggered_rules)
+ # Check the suggestions made by the triggered rules
+ for sugg in rule.get_suggestions():
+ self.assertIn(sugg, RuleToSuggestions[rule.name])
+
+ for rule in triggered_rules:
+ self.assertIn(rule, rules_dict.values())
+ for sugg in RuleToSuggestions[rule.name]:
+ self.assertIn(sugg, rule.get_suggestions())
+
+
+class TestConditionsConjunctions(unittest.TestCase):
+ def setUp(self):
+ # load the Rules
+ this_path = os.path.abspath(os.path.dirname(__file__))
+ ini_path = os.path.join(this_path, "input_files/test_rules.ini")
+ self.db_rules = RulesSpec(ini_path)
+ self.db_rules.load_rules_from_spec()
+ self.db_rules.perform_section_checks()
+ # load the data sources: LOG and OPTIONS
+ log_path = os.path.join(this_path, "input_files/LOG-1")
+ options_path = os.path.join(this_path, "input_files/OPTIONS-000005")
+ db_options_parser = DatabaseOptions(options_path)
+ self.column_families = db_options_parser.get_column_families()
+ db_logs_parser = DatabaseLogs(log_path, self.column_families)
+ self.data_sources = {
+ DataSource.Type.DB_OPTIONS: [db_options_parser],
+ DataSource.Type.LOG: [db_logs_parser],
+ }
+
+ def test_condition_conjunctions(self):
+ conditions_dict = self.db_rules.get_conditions_dict()
+ rules_dict = self.db_rules.get_rules_dict()
+ # Make sure none of the conditions is triggered beforehand
+ for cond in conditions_dict.values():
+ self.assertFalse(cond.is_triggered(), repr(cond))
+ for rule in rules_dict.values():
+ self.assertFalse(
+ rule.is_triggered(conditions_dict, self.column_families), repr(rule)
+ )
+
+ # Trigger the conditions as per the data sources.
+ self.db_rules.trigger_conditions(self.data_sources)
+
+ # Check for the conditions
+ conds_triggered = ["log-1-true", "log-2-true", "log-3-true"]
+ conds_not_triggered = ["log-4-false", "options-1-false"]
+ for cond in conds_triggered:
+ self.assertTrue(conditions_dict[cond].is_triggered(), repr(cond))
+ for cond in conds_not_triggered:
+ self.assertFalse(conditions_dict[cond].is_triggered(), repr(cond))
+
+ # Check for the rules
+ rules_triggered = ["multiple-conds-true"]
+ rules_not_triggered = [
+ "single-condition-false",
+ "multiple-conds-one-false",
+ "multiple-conds-all-false",
+ ]
+ for rule_name in rules_triggered:
+ rule = rules_dict[rule_name]
+ self.assertTrue(
+ rule.is_triggered(conditions_dict, self.column_families), repr(rule)
+ )
+ for rule_name in rules_not_triggered:
+ rule = rules_dict[rule_name]
+ self.assertFalse(
+ rule.is_triggered(conditions_dict, self.column_families), repr(rule)
+ )
+
+
+class TestSanityChecker(unittest.TestCase):
+ def setUp(self):
+ this_path = os.path.abspath(os.path.dirname(__file__))
+ ini_path = os.path.join(this_path, "input_files/rules_err1.ini")
+ db_rules = RulesSpec(ini_path)
+ db_rules.load_rules_from_spec()
+ self.rules_dict = db_rules.get_rules_dict()
+ self.conditions_dict = db_rules.get_conditions_dict()
+ self.suggestions_dict = db_rules.get_suggestions_dict()
+
+ def test_rule_missing_suggestions(self):
+ regex = ".*rule must have at least one suggestion.*"
+ with self.assertRaisesRegex(ValueError, regex):
+ self.rules_dict["missing-suggestions"].perform_checks()
+
+ def test_rule_missing_conditions(self):
+ regex = ".*rule must have at least one condition.*"
+ with self.assertRaisesRegex(ValueError, regex):
+ self.rules_dict["missing-conditions"].perform_checks()
+
+ def test_condition_missing_regex(self):
+ regex = ".*provide regex for log condition.*"
+ with self.assertRaisesRegex(ValueError, regex):
+ self.conditions_dict["missing-regex"].perform_checks()
+
+ def test_condition_missing_options(self):
+ regex = ".*options missing in condition.*"
+ with self.assertRaisesRegex(ValueError, regex):
+ self.conditions_dict["missing-options"].perform_checks()
+
+ def test_condition_missing_expression(self):
+ regex = ".*expression missing in condition.*"
+ with self.assertRaisesRegex(ValueError, regex):
+ self.conditions_dict["missing-expression"].perform_checks()
+
+ def test_suggestion_missing_option(self):
+ regex = ".*provide option or description.*"
+ with self.assertRaisesRegex(ValueError, regex):
+ self.suggestions_dict["missing-option"].perform_checks()
+
+ def test_suggestion_missing_description(self):
+ regex = ".*provide option or description.*"
+ with self.assertRaisesRegex(ValueError, regex):
+ self.suggestions_dict["missing-description"].perform_checks()
+
+
+class TestParsingErrors(unittest.TestCase):
+ def setUp(self):
+ self.this_path = os.path.abspath(os.path.dirname(__file__))
+
+ def test_condition_missing_source(self):
+ ini_path = os.path.join(self.this_path, "input_files/rules_err2.ini")
+ db_rules = RulesSpec(ini_path)
+ regex = ".*provide source for condition.*"
+ with self.assertRaisesRegex(NotImplementedError, regex):
+ db_rules.load_rules_from_spec()
+
+ def test_suggestion_missing_action(self):
+ ini_path = os.path.join(self.this_path, "input_files/rules_err3.ini")
+ db_rules = RulesSpec(ini_path)
+ regex = ".*provide action for option.*"
+ with self.assertRaisesRegex(ValueError, regex):
+ db_rules.load_rules_from_spec()
+
+ def test_section_no_name(self):
+ ini_path = os.path.join(self.this_path, "input_files/rules_err4.ini")
+ db_rules = RulesSpec(ini_path)
+ regex = "Parsing error: needed section header:.*"
+ with self.assertRaisesRegex(ValueError, regex):
+ db_rules.load_rules_from_spec()
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/src/rocksdb/tools/analyze_txn_stress_test.sh b/src/rocksdb/tools/analyze_txn_stress_test.sh
new file mode 100755
index 000000000..477b1fac5
--- /dev/null
+++ b/src/rocksdb/tools/analyze_txn_stress_test.sh
@@ -0,0 +1,77 @@
+#!/bin/bash
+# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
+# Usage:
+# 1. Enable ROCKS_LOG_DETAILS in util/logging.h
+# 2. Run ./transaction_test --gtest_filter="MySQLStyleTransactionTest/MySQLStyleTransactionTest.TransactionStressTest/*" --gtest_break_on_failure
+# 3. SET=1 # 2 or 3
+# 4. LOG=/dev/shm/transaction_testdb_8600601584148590297/LOG
+# 5. grep RandomTransactionVerify $LOG | cut -d' ' -f 12 | sort -n # to find verify snapshots
+# 5. vn=1345
+# 6. vn_1=1340
+# 4. . tools/tools/analyze_txn_stress_test.sh
+echo Input params:
+# The rocksdb LOG path
+echo $LOG
+# Snapshot at which we got RandomTransactionVerify failure
+echo $vn
+# The snapshot before that where RandomTransactionVerify passed
+echo $vn_1
+# The stress tests use 3 sets, one or more might have shown inconsistent results.
+SET=${SET-1} # 1 or 2 or 3
+echo Checking set number $SET
+
+# Find the txns that committed between the two snapshots, and gather their changes made by them in /tmp/changes.txt
+# 2019/02/28-15:25:51.655477 7fffec9ff700 [DEBUG] [ilities/transactions/write_prepared_txn_db.cc:416] Txn 68497 Committing with 68498
+grep Committing $LOG | awk '{if ($9 <= vn && $9 > vn_1) print $0}' vn=$vn vn_1=${vn_1} > /tmp/txn.txt
+# 2019/02/28-15:25:49.046464 7fffe81f5700 [DEBUG] [il/transaction_test_util.cc:216] Commit of 65541 OK (txn12936193128775589751-9089)
+for i in `cat /tmp/txn.txt | awk '{print $6}'`; do grep "Commit of $i " $LOG; done > /tmp/names.txt
+for n in `cat /tmp/names.txt | awk '{print $9}'`; do grep $n $LOG; done > /tmp/changes.txt
+echo "Sum of the changes:"
+cat /tmp/changes.txt | grep Insert | awk '{print $12}' | cut -d= -f1 | cut -d+ -f2 | awk '{sum+=$1} END{print sum}'
+
+# Gather read values at each snapshot
+# 2019/02/28-15:25:51.655926 7fffebbff700 [DEBUG] [il/transaction_test_util.cc:347] VerifyRead at 67972 (67693): 000230 value: 15983
+grep "VerifyRead at ${vn_1} (.*): 000${SET}" $LOG | cut -d' ' -f 9- > /tmp/va.txt
+grep "VerifyRead at ${vn} (.*): 000${SET}" $LOG | cut -d' ' -f 9- > /tmp/vb.txt
+
+# For each key in the 2nd snapshot, find the value read by 1st, do the adds, and see if the results match.
+IFS=$'\n'
+for l in `cat /tmp/vb.txt`;
+do
+ grep $l /tmp/va.txt > /dev/null ;
+ if [[ $? -ne 0 ]]; then
+ #echo $l
+ k=`echo $l | awk '{print $1}'`;
+ v=`echo $l | awk '{print $3}'`;
+ # 2019/02/28-15:25:19.350111 7fffe81f5700 [DEBUG] [il/transaction_test_util.cc:194] Insert (txn12936193128775589751-2298) OK snap: 16289 key:000219 value: 3772+95=3867
+ exp=`grep "\<$k\>" /tmp/changes.txt | tail -1 | cut -d= -f2`;
+ if [[ $v -ne $exp ]]; then echo $l; fi
+ else
+ k=`echo $l | awk '{print $1}'`;
+ grep "\<$k\>" /tmp/changes.txt
+ fi;
+done
+
+# Check that all the keys read in the 1st snapshot are still visible in the 2nd
+for l in `cat /tmp/va.txt`;
+do
+ k=`echo $l | awk '{print $1}'`;
+ grep "\<$k\>" /tmp/vb.txt > /dev/null
+ if [[ $? -ne 0 ]]; then
+ echo missing key $k
+ fi
+done
+
+# The following found a bug in ValidateSnapshot. It checks if the adds on each key match up.
+grep Insert /tmp/changes.txt | cut -d' ' -f 10 | sort | uniq > /tmp/keys.txt
+for k in `cat /tmp/keys.txt`;
+do
+ grep "\<$k\>" /tmp/changes.txt > /tmp/adds.txt;
+ # 2019/02/28-15:25:19.350111 7fffe81f5700 [DEBUG] [il/transaction_test_util.cc:194] Insert (txn12936193128775589751-2298) OK snap: 16289 key:000219 value: 3772+95=3867
+ START=`head -1 /tmp/adds.txt | cut -d' ' -f 12 | cut -d+ -f1`
+ END=`tail -1 /tmp/adds.txt | cut -d' ' -f 12 | cut -d= -f2`
+ ADDS=`cat /tmp/adds.txt | grep Insert | awk '{print $12}' | cut -d= -f1 | cut -d+ -f2 | awk '{sum+=$1} END{print sum}'`
+ EXP=$((START+ADDS))
+ # If first + all the adds != last then there was an issue with ValidateSnapshot.
+ if [[ $END -ne $EXP ]]; then echo inconsistent txn: $k $START+$ADDS=$END; cat /tmp/adds.txt; return 1; fi
+done
diff --git a/src/rocksdb/tools/auto_sanity_test.sh b/src/rocksdb/tools/auto_sanity_test.sh
new file mode 100755
index 000000000..4670ef9bb
--- /dev/null
+++ b/src/rocksdb/tools/auto_sanity_test.sh
@@ -0,0 +1,93 @@
+# shellcheck disable=SC2148
+# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
+TMP_DIR="${TMPDIR:-/tmp}/rocksdb-sanity-test"
+
+if [ "$#" -lt 2 ]; then
+ echo "usage: ./auto_sanity_test.sh [new_commit] [old_commit]"
+ echo "Missing either [new_commit] or [old_commit], perform sanity check with the latest and 10th latest commits."
+ recent_commits=`git log | grep -e "^commit [a-z0-9]\+$"| head -n10 | sed -e 's/commit //g'`
+ commit_new=`echo "$recent_commits" | head -n1`
+ commit_old=`echo "$recent_commits" | tail -n1`
+ echo "the most recent commits are:"
+ echo "$recent_commits"
+else
+ commit_new=$1
+ commit_old=$2
+fi
+
+if [ ! -d $TMP_DIR ]; then
+ mkdir $TMP_DIR
+fi
+dir_new="${TMP_DIR}/${commit_new}"
+dir_old="${TMP_DIR}/${commit_old}"
+
+function makestuff() {
+ echo "make clean"
+ make clean > /dev/null
+ echo "make db_sanity_test -j32"
+ make db_sanity_test -j32 > /dev/null
+ if [ $? -ne 0 ]; then
+ echo "[ERROR] Failed to perform 'make db_sanity_test'"
+ exit 1
+ fi
+}
+
+rm -r -f $dir_new
+rm -r -f $dir_old
+
+echo "Running db sanity check with commits $commit_new and $commit_old."
+
+echo "============================================================="
+echo "Making build $commit_new"
+git checkout $commit_new
+if [ $? -ne 0 ]; then
+ echo "[ERROR] Can't checkout $commit_new"
+ exit 1
+fi
+makestuff
+mv db_sanity_test new_db_sanity_test
+echo "Creating db based on the new commit --- $commit_new"
+./new_db_sanity_test $dir_new create
+cp ./tools/db_sanity_test.cc $dir_new
+cp ./tools/auto_sanity_test.sh $dir_new
+
+echo "============================================================="
+echo "Making build $commit_old"
+git checkout $commit_old
+if [ $? -ne 0 ]; then
+ echo "[ERROR] Can't checkout $commit_old"
+ exit 1
+fi
+cp -f $dir_new/db_sanity_test.cc ./tools/.
+cp -f $dir_new/auto_sanity_test.sh ./tools/.
+makestuff
+mv db_sanity_test old_db_sanity_test
+echo "Creating db based on the old commit --- $commit_old"
+./old_db_sanity_test $dir_old create
+
+echo "============================================================="
+echo "[Backward Compatibility Check]"
+echo "Verifying old db $dir_old using the new commit --- $commit_new"
+./new_db_sanity_test $dir_old verify
+if [ $? -ne 0 ]; then
+ echo "[ERROR] Backward Compatibility Check fails:"
+ echo " Verification of $dir_old using commit $commit_new failed."
+ exit 2
+fi
+
+echo "============================================================="
+echo "[Forward Compatibility Check]"
+echo "Verifying new db $dir_new using the old commit --- $commit_old"
+./old_db_sanity_test $dir_new verify
+if [ $? -ne 0 ]; then
+ echo "[ERROR] Forward Compatibility Check fails:"
+ echo " $dir_new using commit $commit_old failed."
+ exit 2
+fi
+
+rm old_db_sanity_test
+rm new_db_sanity_test
+rm -rf $dir_new
+rm -rf $dir_old
+
+echo "Auto sanity test passed!"
diff --git a/src/rocksdb/tools/backup_db.sh b/src/rocksdb/tools/backup_db.sh
new file mode 100755
index 000000000..aa82f1dba
--- /dev/null
+++ b/src/rocksdb/tools/backup_db.sh
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
+#
+#
+
+if [ "$#" -lt 2 ]; then
+ echo "usage: ${BASH_SOURCE[0]} <DB Path> <Backup Dir>"
+ exit 1
+fi
+
+db_dir="$1"
+backup_dir="$2"
+
+echo "== Backing up DB $db_dir to $backup_dir"
+./ldb backup --db="$db_dir" --backup_dir="$backup_dir"
diff --git a/src/rocksdb/tools/benchmark.sh b/src/rocksdb/tools/benchmark.sh
new file mode 100755
index 000000000..b41d25c78
--- /dev/null
+++ b/src/rocksdb/tools/benchmark.sh
@@ -0,0 +1,1173 @@
+#!/usr/bin/env bash
+# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
+# REQUIRE: db_bench binary exists in the current directory
+
+# Exit Codes
+EXIT_INVALID_ARGS=1
+EXIT_NOT_COMPACTION_TEST=2
+EXIT_UNKNOWN_JOB=3
+
+# Size Constants
+K=1024
+M=$((1024 * K))
+G=$((1024 * M))
+T=$((1024 * G))
+
+function display_usage() {
+ echo "usage: benchmark.sh [--help] <test>"
+ echo ""
+ echo "These are the available benchmark tests:"
+ echo -e "\tbulkload"
+ echo -e "\tfillseq_disable_wal\t\tSequentially fill the database with no WAL"
+ echo -e "\tfillseq_enable_wal\t\tSequentially fill the database with WAL"
+ echo -e "\toverwrite"
+ echo -e "\tupdaterandom"
+ echo -e "\treadrandom"
+ echo -e "\tmergerandom"
+ echo -e "\tfilluniquerandom"
+ echo -e "\tmultireadrandom"
+ echo -e "\tfwdrange"
+ echo -e "\trevrange"
+ echo -e "\treadwhilewriting"
+ echo -e "\treadwhilemerging"
+ echo -e "\tfwdrangewhilewriting"
+ echo -e "\trevrangewhilewriting"
+ echo -e "\tfwdrangewhilemerging"
+ echo -e "\trevrangewhilemerging"
+ echo -e "\trandomtransaction"
+ echo -e "\tuniversal_compaction"
+ echo -e "\tdebug"
+ echo ""
+ echo "Generic enviroment Variables:"
+ echo -e "\tJOB_ID\t\t\t\tAn identifier for the benchmark job, will appear in the results"
+ echo -e "\tDB_DIR\t\t\t\tPath to write the database data directory"
+ echo -e "\tWAL_DIR\t\t\t\tPath to write the database WAL directory"
+ echo -e "\tOUTPUT_DIR\t\t\tPath to write the benchmark results to (default: /tmp)"
+ echo -e "\tNUM_KEYS\t\t\tThe number of keys to use in the benchmark"
+ echo -e "\tKEY_SIZE\t\t\tThe size of the keys to use in the benchmark (default: 20 bytes)"
+ echo -e "\tVALUE_SIZE\t\t\tThe size of the values to use in the benchmark (default: 400 bytes)"
+ echo -e "\tBLOCK_SIZE\t\t\tThe size of the database blocks in the benchmark (default: 8 KB)"
+ echo -e "\tDB_BENCH_NO_SYNC\t\tDisable fsync on the WAL"
+ echo -e "\tNUMACTL\t\t\t\tWhen defined use numactl --interleave=all"
+ echo -e "\tNUM_THREADS\t\t\tThe number of threads to use (default: 64)"
+ echo -e "\tMB_WRITE_PER_SEC\t\t\tRate limit for background writer"
+ echo -e "\tNUM_NEXTS_PER_SEEK\t\t(default: 10)"
+ echo -e "\tCACHE_SIZE\t\t\tSize of the block cache (default: 16GB)"
+ echo -e "\tCACHE_NUMSHARDBITS\t\t\tNumber of shards for the block cache is 2 ** cache_numshardbits (default: 6)"
+ echo -e "\tCOMPRESSION_MAX_DICT_BYTES"
+ echo -e "\tCOMPRESSION_TYPE\t\tDefault compression(default: zstd)"
+ echo -e "\tBOTTOMMOST_COMPRESSION\t\t(default: none)"
+ echo -e "\tMIN_LEVEL_TO_COMPRESS\t\tValue for min_level_to_compress for Leveled"
+ echo -e "\tCOMPRESSION_SIZE_PERCENT\tValue for compression_size_percent for Universal"
+ echo -e "\tDURATION\t\t\tNumber of seconds for which the test runs"
+ echo -e "\tWRITES\t\t\t\tNumber of writes for which the test runs"
+ echo -e "\tWRITE_BUFFER_SIZE_MB\t\tThe size of the write buffer in MB (default: 128)"
+ echo -e "\tTARGET_FILE_SIZE_BASE_MB\tThe value for target_file_size_base in MB (default: 128)"
+ echo -e "\tMAX_BYTES_FOR_LEVEL_BASE_MB\tThe value for max_bytes_for_level_base in MB (default: 128)"
+ echo -e "\tMAX_BACKGROUND_JOBS\t\tThe value for max_background_jobs (default: 16)"
+ echo -e "\tCACHE_INDEX_AND_FILTER_BLOCKS\tThe value for cache_index_and_filter_blocks (default: 0)"
+ echo -e "\tUSE_O_DIRECT\t\t\tUse O_DIRECT for user reads and compaction"
+ echo -e "\tBYTES_PER_SYNC\t\t\tValue for bytes_per_sync, set to zero when USE_O_DIRECT is true"
+ echo -e "\tSTATS_INTERVAL_SECONDS\t\tValue for stats_interval_seconds"
+ echo -e "\tREPORT_INTERVAL_SECONDS\t\tValue for report_interval_seconds"
+ echo -e "\tSUBCOMPACTIONS\t\t\tValue for subcompactions"
+ echo -e "\tCOMPACTION_STYLE\t\tOne of leveled, universal, blob. Default is leveled."
+ echo -e "\nEnvironment variables (mostly) for leveled compaction:"
+ echo -e "\tLEVEL0_FILE_NUM_COMPACTION_TRIGGER\t\tValue for level0_file_num_compaction_trigger"
+ echo -e "\tLEVEL0_SLOWDOWN_WRITES_TRIGGER\t\t\tValue for level0_slowdown_writes_trigger"
+ echo -e "\tLEVEL0_STOP_WRITES_TRIGGER\t\t\tValue for level0_stop_writes_trigger"
+ echo -e "\tPER_LEVEL_FANOUT\t\t\t\tValue for max_bytes_for_level_multiplier"
+ echo -e "\tSOFT_PENDING_COMPACTION_BYTES_LIMIT_IN_GB\tThe value for soft_pending_compaction_bytes_limit in GB"
+ echo -e "\tHARD_PENDING_COMPACTION_BYTES_LIMIT_IN_GB\tThe value for hard_pending_compaction_bytes_limit in GB"
+ echo -e "\nEnvironment variables for universal compaction:"
+ echo -e "\tUNIVERSAL_MIN_MERGE_WIDTH\tValue of min_merge_width option for universal"
+ echo -e "\tUNIVERSAL_MAX_MERGE_WIDTH\tValue of min_merge_width option for universal"
+ echo -e "\tUNIVERSAL_SIZE_RATIO\t\tValue of size_ratio option for universal"
+ echo -e "\tUNIVERSAL_MAX_SIZE_AMP\t\tmax_size_amplification_percent for universal"
+ echo -e "\tUNIVERSAL_ALLOW_TRIVIAL_MOVE\tSet allow_trivial_move to true for universal, default is false"
+ echo -e "\nOptions for integrated BlobDB"
+ echo -e "\tMIN_BLOB_SIZE\tValue for min_blob_size"
+ echo -e "\tBLOB_FILE_SIZE\tValue for blob_file_size"
+ echo -e "\tBLOB_COMPRESSION_TYPE\tValue for blob_compression_type"
+ echo -e "\tBLOB_GC_AGE_CUTOFF\tValue for blob_garbage_collection_age_cutoff"
+ echo -e "\tBLOB_GC_FORCE_THRESHOLD\tValue for blob_garbage_collection_force_threshold"
+ echo -e "\tBLOB_FILE_STARTING_LEVEL\t\tBlob file starting level (default: 0)"
+ echo -e "\tUSE_BLOB_CACHE\t\t\tEnable blob cache (default: 1)"
+ echo -e "\tUSE_SHARED_BLOCK_AND_BLOB_CACHE\t\t\tUse the same backing cache for block cache and blob cache (default: 1)"
+ echo -e "\tBLOB_CACHE_SIZE\t\t\tSize of the blob cache (default: 16GB)"
+ echo -e "\tBLOB_CACHE_NUMSHARDBITS\t\t\tNumber of shards for the blob cache is 2 ** blob_cache_numshardbits (default: 6)"
+ echo -e "\tPREPOPULATE_BLOB_CACHE\t\t\tPre-populate hot/warm blobs in blob cache (default: 0)"
+}
+
+if [ $# -lt 1 ]; then
+ display_usage
+ exit $EXIT_INVALID_ARGS
+fi
+bench_cmd=$1
+shift
+bench_args=$*
+
+if [[ "$bench_cmd" == "--help" ]]; then
+ display_usage
+ exit
+fi
+
+job_id=${JOB_ID}
+
+# Make it easier to run only the compaction test. Getting valid data requires
+# a number of iterations and having an ability to run the test separately from
+# rest of the benchmarks helps.
+if [ "$COMPACTION_TEST" == "1" -a "$bench_cmd" != "universal_compaction" ]; then
+ echo "Skipping $1 because it's not a compaction test."
+ exit $EXIT_NOT_COMPACTION_TEST
+fi
+
+if [ -z $DB_DIR ]; then
+ echo "DB_DIR is not defined"
+ exit $EXIT_INVALID_ARGS
+fi
+
+if [ -z $WAL_DIR ]; then
+ echo "WAL_DIR is not defined"
+ exit $EXIT_INVALID_ARGS
+fi
+
+output_dir=${OUTPUT_DIR:-/tmp}
+if [ ! -d $output_dir ]; then
+ mkdir -p $output_dir
+fi
+
+report="$output_dir/report.tsv"
+schedule="$output_dir/schedule.txt"
+
+# all multithreaded tests run with sync=1 unless
+# $DB_BENCH_NO_SYNC is defined
+syncval="1"
+if [ ! -z $DB_BENCH_NO_SYNC ]; then
+ echo "Turning sync off for all multithreaded tests"
+ syncval="0";
+fi
+
+compaction_style=${COMPACTION_STYLE:-leveled}
+if [ $compaction_style = "leveled" ]; then
+ echo Use leveled compaction
+elif [ $compaction_style = "universal" ]; then
+ echo Use universal compaction
+elif [ $compaction_style = "blob" ]; then
+ echo Use blob compaction
+else
+ echo COMPACTION_STYLE is :: $COMPACTION_STYLE :: and must be one of leveled, universal, blob
+ exit $EXIT_INVALID_ARGS
+fi
+
+num_threads=${NUM_THREADS:-64}
+mb_written_per_sec=${MB_WRITE_PER_SEC:-0}
+# Only for tests that do range scans
+num_nexts_per_seek=${NUM_NEXTS_PER_SEEK:-10}
+cache_size=${CACHE_SIZE:-$(( 16 * $G ))}
+cache_numshardbits=${CACHE_NUMSHARDBITS:-6}
+compression_max_dict_bytes=${COMPRESSION_MAX_DICT_BYTES:-0}
+compression_type=${COMPRESSION_TYPE:-zstd}
+min_level_to_compress=${MIN_LEVEL_TO_COMPRESS:-"-1"}
+compression_size_percent=${COMPRESSION_SIZE_PERCENT:-"-1"}
+
+duration=${DURATION:-0}
+writes=${WRITES:-0}
+
+num_keys=${NUM_KEYS:-8000000000}
+key_size=${KEY_SIZE:-20}
+value_size=${VALUE_SIZE:-400}
+block_size=${BLOCK_SIZE:-8192}
+write_buffer_mb=${WRITE_BUFFER_SIZE_MB:-128}
+target_file_mb=${TARGET_FILE_SIZE_BASE_MB:-128}
+l1_mb=${MAX_BYTES_FOR_LEVEL_BASE_MB:-1024}
+max_background_jobs=${MAX_BACKGROUND_JOBS:-16}
+stats_interval_seconds=${STATS_INTERVAL_SECONDS:-60}
+report_interval_seconds=${REPORT_INTERVAL_SECONDS:-1}
+subcompactions=${SUBCOMPACTIONS:-1}
+per_level_fanout=${PER_LEVEL_FANOUT:-8}
+
+cache_index_and_filter=${CACHE_INDEX_AND_FILTER_BLOCKS:-0}
+if [[ $cache_index_and_filter -eq 0 ]]; then
+ cache_meta_flags=""
+elif [[ $cache_index_and_filter -eq 1 ]]; then
+ cache_meta_flags="\
+ --cache_index_and_filter_blocks=$cache_index_and_filter \
+ --cache_high_pri_pool_ratio=0.5 --cache_low_pri_pool_ratio=0"
+else
+ echo CACHE_INDEX_AND_FILTER_BLOCKS was $CACHE_INDEX_AND_FILTER_BLOCKS but must be 0 or 1
+ exit $EXIT_INVALID_ARGS
+fi
+
+soft_pending_arg=""
+if [ ! -z $SOFT_PENDING_COMPACTION_BYTES_LIMIT_IN_GB ]; then
+ soft_pending_bytes=$( echo $SOFT_PENDING_COMPACTION_BYTES_LIMIT_IN_GB | \
+ awk '{ printf "%.0f", $1 * GB }' GB=$G )
+ soft_pending_arg="--soft_pending_compaction_bytes_limit=$soft_pending_bytes"
+fi
+
+hard_pending_arg=""
+if [ ! -z $HARD_PENDING_COMPACTION_BYTES_LIMIT_IN_GB ]; then
+ hard_pending_bytes=$( echo $HARD_PENDING_COMPACTION_BYTES_LIMIT_IN_GB | \
+ awk '{ printf "%.0f", $1 * GB }' GB=$G )
+ hard_pending_arg="--hard_pending_compaction_bytes_limit=$hard_pending_bytes"
+fi
+
+o_direct_flags=""
+if [ ! -z $USE_O_DIRECT ]; then
+ # Some of these flags are only supported in new versions and --undefok makes that work
+ o_direct_flags="--use_direct_reads --use_direct_io_for_flush_and_compaction --prepopulate_block_cache=1"
+ bytes_per_sync=0
+else
+ bytes_per_sync=${BYTES_PER_SYNC:-$(( 1 * M ))}
+fi
+
+univ_min_merge_width=${UNIVERSAL_MIN_MERGE_WIDTH:-2}
+univ_max_merge_width=${UNIVERSAL_MAX_MERGE_WIDTH:-20}
+univ_size_ratio=${UNIVERSAL_SIZE_RATIO:-1}
+univ_max_size_amp=${UNIVERSAL_MAX_SIZE_AMP:-200}
+
+if [ ! -z $UNIVERSAL_ALLOW_TRIVIAL_MOVE ]; then
+ univ_allow_trivial_move=1
+else
+ univ_allow_trivial_move=0
+fi
+
+min_blob_size=${MIN_BLOB_SIZE:-0}
+blob_file_size=${BLOB_FILE_SIZE:-$(( 256 * $M ))}
+blob_compression_type=${BLOB_COMPRESSION_TYPE:-${compression_type}}
+blob_gc_age_cutoff=${BLOB_GC_AGE_CUTOFF:-"0.25"}
+blob_gc_force_threshold=${BLOB_GC_FORCE_THRESHOLD:-1}
+blob_file_starting_level=${BLOB_FILE_STARTING_LEVEL:-0}
+use_blob_cache=${USE_BLOB_CACHE:-1}
+use_shared_block_and_blob_cache=${USE_SHARED_BLOCK_AND_BLOB_CACHE:-1}
+blob_cache_size=${BLOB_CACHE_SIZE:-$(( 16 * $G ))}
+blob_cache_numshardbits=${BLOB_CACHE_NUMSHARDBITS:-6}
+prepopulate_blob_cache=${PREPOPULATE_BLOB_CACHE:-0}
+
+# This script still works back to RocksDB 6.0
+undef_params="\
+use_blob_cache,\
+use_shared_block_and_blob_cache,\
+blob_cache_size,blob_cache_numshardbits,\
+prepopulate_blob_cache,\
+multiread_batched,\
+cache_low_pri_pool_ratio,\
+prepopulate_block_cache"
+
+const_params_base="
+ --undefok=$undef_params \
+ --db=$DB_DIR \
+ --wal_dir=$WAL_DIR \
+ \
+ --num=$num_keys \
+ --key_size=$key_size \
+ --value_size=$value_size \
+ --block_size=$block_size \
+ --cache_size=$cache_size \
+ --cache_numshardbits=$cache_numshardbits \
+ --compression_max_dict_bytes=$compression_max_dict_bytes \
+ --compression_ratio=0.5 \
+ --compression_type=$compression_type \
+ --bytes_per_sync=$bytes_per_sync \
+ $cache_meta_flags \
+ $o_direct_flags \
+ --benchmark_write_rate_limit=$(( 1024 * 1024 * $mb_written_per_sec )) \
+ \
+ --write_buffer_size=$(( $write_buffer_mb * M)) \
+ --target_file_size_base=$(( $target_file_mb * M)) \
+ --max_bytes_for_level_base=$(( $l1_mb * M)) \
+ \
+ --verify_checksum=1 \
+ --delete_obsolete_files_period_micros=$((60 * M)) \
+ --max_bytes_for_level_multiplier=$per_level_fanout \
+ \
+ --statistics=0 \
+ --stats_per_interval=1 \
+ --stats_interval_seconds=$stats_interval_seconds \
+ --report_interval_seconds=$report_interval_seconds \
+ --histogram=1 \
+ \
+ --memtablerep=skip_list \
+ --bloom_bits=10 \
+ --open_files=-1 \
+ --subcompactions=$subcompactions \
+ \
+ $bench_args"
+
+level_const_params="
+ $const_params_base \
+ --compaction_style=0 \
+ --num_levels=8 \
+ --min_level_to_compress=$min_level_to_compress \
+ --level_compaction_dynamic_level_bytes=true \
+ --pin_l0_filter_and_index_blocks_in_cache=1 \
+ $soft_pending_arg \
+ $hard_pending_arg \
+"
+
+# These inherit level_const_params because the non-blob LSM tree uses leveled compaction.
+blob_const_params="
+ $level_const_params \
+ --enable_blob_files=true \
+ --min_blob_size=$min_blob_size \
+ --blob_file_size=$blob_file_size \
+ --blob_compression_type=$blob_compression_type \
+ --enable_blob_garbage_collection=true \
+ --blob_garbage_collection_age_cutoff=$blob_gc_age_cutoff \
+ --blob_garbage_collection_force_threshold=$blob_gc_force_threshold \
+ --blob_file_starting_level=$blob_file_starting_level \
+ --use_blob_cache=$use_blob_cache \
+ --use_shared_block_and_blob_cache=$use_shared_block_and_blob_cache \
+ --blob_cache_size=$blob_cache_size \
+ --blob_cache_numshardbits=$blob_cache_numshardbits \
+ --prepopulate_blob_cache=$prepopulate_blob_cache \
+"
+
+# TODO:
+# pin_l0_filter_and..., is this OK?
+univ_const_params="
+ $const_params_base \
+ --compaction_style=1 \
+ --num_levels=40 \
+ --universal_compression_size_percent=$compression_size_percent \
+ --pin_l0_filter_and_index_blocks_in_cache=1 \
+ --universal_min_merge_width=$univ_min_merge_width \
+ --universal_max_merge_width=$univ_max_merge_width \
+ --universal_size_ratio=$univ_size_ratio \
+ --universal_max_size_amplification_percent=$univ_max_size_amp \
+ --universal_allow_trivial_move=$univ_allow_trivial_move \
+"
+
+if [ $compaction_style == "leveled" ]; then
+ const_params="$level_const_params"
+ l0_file_num_compaction_trigger=${LEVEL0_FILE_NUM_COMPACTION_TRIGGER:-4}
+ l0_slowdown_writes_trigger=${LEVEL0_SLOWDOWN_WRITES_TRIGGER:-20}
+ l0_stop_writes_trigger=${LEVEL0_STOP_WRITES_TRIGGER:-30}
+elif [ $compaction_style == "universal" ]; then
+ const_params="$univ_const_params"
+ l0_file_num_compaction_trigger=${LEVEL0_FILE_NUM_COMPACTION_TRIGGER:-8}
+ l0_slowdown_writes_trigger=${LEVEL0_SLOWDOWN_WRITES_TRIGGER:-20}
+ l0_stop_writes_trigger=${LEVEL0_STOP_WRITES_TRIGGER:-30}
+else
+ # compaction_style == "blob"
+ const_params="$blob_const_params"
+ l0_file_num_compaction_trigger=${LEVEL0_FILE_NUM_COMPACTION_TRIGGER:-4}
+ l0_slowdown_writes_trigger=${LEVEL0_SLOWDOWN_WRITES_TRIGGER:-20}
+ l0_stop_writes_trigger=${LEVEL0_STOP_WRITES_TRIGGER:-30}
+fi
+
+l0_config="
+ --level0_file_num_compaction_trigger=$l0_file_num_compaction_trigger \
+ --level0_slowdown_writes_trigger=$l0_slowdown_writes_trigger \
+ --level0_stop_writes_trigger=$l0_stop_writes_trigger"
+
+# You probably don't want to set both --writes and --duration
+if [ $duration -gt 0 ]; then
+ const_params="$const_params --duration=$duration"
+fi
+if [ $writes -gt 0 ]; then
+ const_params="$const_params --writes=$writes"
+fi
+
+params_w="$l0_config \
+ --max_background_jobs=$max_background_jobs \
+ --max_write_buffer_number=8 \
+ $const_params"
+
+params_bulkload="--max_background_jobs=$max_background_jobs \
+ --max_write_buffer_number=8 \
+ --allow_concurrent_memtable_write=false \
+ --level0_file_num_compaction_trigger=$((10 * M)) \
+ --level0_slowdown_writes_trigger=$((10 * M)) \
+ --level0_stop_writes_trigger=$((10 * M)) \
+ $const_params "
+
+params_fillseq="--allow_concurrent_memtable_write=false \
+ $params_w "
+
+#
+# Tune values for level and universal compaction.
+# For universal compaction, these level0_* options mean total sorted of runs in
+# LSM. In level-based compaction, it means number of L0 files.
+#
+params_level_compact="$const_params \
+ --max_background_flushes=4 \
+ --max_write_buffer_number=4 \
+ --level0_file_num_compaction_trigger=4 \
+ --level0_slowdown_writes_trigger=16 \
+ --level0_stop_writes_trigger=20"
+
+params_univ_compact="$const_params \
+ --max_background_flushes=4 \
+ --max_write_buffer_number=4 \
+ --level0_file_num_compaction_trigger=8 \
+ --level0_slowdown_writes_trigger=16 \
+ --level0_stop_writes_trigger=20"
+
+tsv_header="ops_sec\tmb_sec\tlsm_sz\tblob_sz\tc_wgb\tw_amp\tc_mbps\tc_wsecs\tc_csecs\tb_rgb\tb_wgb\tusec_op\tp50\tp99\tp99.9\tp99.99\tpmax\tuptime\tstall%\tNstall\tu_cpu\ts_cpu\trss\ttest\tdate\tversion\tjob_id\tgithash"
+
+function get_cmd() {
+ output=$1
+
+ numa=""
+ if [ ! -z $NUMACTL ]; then
+ numa="numactl --interleave=all "
+ fi
+
+ # Try to use timeout when duration is set because some tests (revrange*) hang
+ # for some versions (v6.10, v6.11).
+ timeout_cmd=""
+ if [ $duration -gt 0 ]; then
+ if hash timeout ; then
+ timeout_cmd="timeout $(( $duration + 600 ))"
+ fi
+ fi
+
+ echo "/usr/bin/time -f '%e %U %S' -o $output $numa $timeout_cmd"
+}
+
+function month_to_num() {
+ local date_str=$1
+ date_str="${date_str/Jan/01}"
+ date_str="${date_str/Feb/02}"
+ date_str="${date_str/Mar/03}"
+ date_str="${date_str/Apr/04}"
+ date_str="${date_str/May/05}"
+ date_str="${date_str/Jun/06}"
+ date_str="${date_str/Jul/07}"
+ date_str="${date_str/Aug/08}"
+ date_str="${date_str/Sep/09}"
+ date_str="${date_str/Oct/10}"
+ date_str="${date_str/Nov/11}"
+ date_str="${date_str/Dec/12}"
+ echo $date_str
+}
+
+function start_stats {
+ output=$1
+ iostat -y -mx 1 >& $output.io &
+ vmstat 1 >& $output.vm &
+ # tail -1 because "ps | grep db_bench" returns 2 entries and we want the second
+ while :; do ps aux | grep db_bench | grep -v grep | tail -1; sleep 10; done >& $output.ps &
+ # This sets a global value
+ pspid=$!
+
+ while :; do
+ b_gb=$( ls -l $DB_DIR 2> /dev/null | grep blob | awk '{ c += 1; b += $5 } END { printf "%.1f", b / (1024*1024*1024) }' )
+ s_gb=$( ls -l $DB_DIR 2> /dev/null | grep sst | awk '{ c += 1; b += $5 } END { printf "%.1f", b / (1024*1024*1024) }' )
+ l_gb=$( ls -l $WAL_DIR 2> /dev/null | grep log | awk '{ c += 1; b += $5 } END { printf "%.1f", b / (1024*1024*1024) }' )
+ a_gb=$( ls -l $DB_DIR 2> /dev/null | awk '{ c += 1; b += $5 } END { printf "%.1f", b / (1024*1024*1024) }' )
+ ts=$( date +%H%M%S )
+ echo -e "${a_gb}\t${s_gb}\t${l_gb}\t${b_gb}\t${ts}"
+ sleep 10
+ done >& $output.sizes &
+ # This sets a global value
+ szpid=$!
+}
+
+function stop_stats {
+ output=$1
+ kill $pspid
+ kill $szpid
+ killall iostat
+ killall vmstat
+ sleep 1
+ gzip $output.io
+ gzip $output.vm
+
+ am=$( sort -nk 1,1 $output.sizes | tail -1 | awk '{ print $1 }' )
+ sm=$( sort -nk 2,2 $output.sizes | tail -1 | awk '{ print $2 }' )
+ lm=$( sort -nk 3,3 $output.sizes | tail -1 | awk '{ print $3 }' )
+ bm=$( sort -nk 4,4 $output.sizes | tail -1 | awk '{ print $4 }' )
+ echo -e "max sizes (GB): $am all, $sm sst, $lm log, $bm blob" >> $output.sizes
+}
+
+function units_as_gb {
+ size=$1
+ units=$2
+
+ case $units in
+ MB)
+ echo "$size" | awk '{ printf "%.1f", $1 / 1024.0 }'
+ ;;
+ GB)
+ echo "$size"
+ ;;
+ TB)
+ echo "$size" | awk '{ printf "%.1f", $1 * 1024.0 }'
+ ;;
+ *)
+ echo "NA"
+ ;;
+ esac
+}
+
+function summarize_result {
+ test_out=$1
+ test_name=$2
+ bench_name=$3
+
+ # In recent versions these can be found directly via db_bench --version, --build_info but
+ # grepping from the log lets this work on older versions.
+ version="$( grep "RocksDB version:" "$DB_DIR"/LOG | head -1 | awk '{ printf "%s", $5 }' )"
+ git_hash="$( grep "Git sha" "$DB_DIR"/LOG | head -1 | awk '{ printf "%s", substr($5, 1, 10) }' )"
+
+ # Note that this function assumes that the benchmark executes long enough so
+ # that "Compaction Stats" is written to stdout at least once. If it won't
+ # happen then empty output from grep when searching for "Sum" will cause
+ # syntax errors.
+ date=$( grep ^Date: $test_out | awk '{ print $6 "-" $3 "-" $4 "T" $5 }' )
+ my_date=$( month_to_num $date )
+ uptime=$( grep ^Uptime\(secs $test_out | tail -1 | awk '{ printf "%.0f", $2 }' )
+ stall_pct=$( grep "^Cumulative stall" $test_out| tail -1 | awk '{ print $5 }' )
+ nstall=$( grep ^Stalls\(count\): $test_out | tail -1 | awk '{ print $2 + $6 + $10 + $14 + $18 + $20 }' )
+
+ if ! grep ^"$bench_name" "$test_out" > /dev/null 2>&1 ; then
+ echo -e "failed\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t$test_name\t$my_date\t$version\t$job_id\t$git_hash"
+ return
+ fi
+
+ # Output formats
+ # V1: readrandom : 10.218 micros/op 3131616 ops/sec; 1254.3 MB/s (176144999 of 176144999 found)
+ # The MB/s is mssing for multireadrandom
+ # V1a: multireadrandom : 10.164 micros/op 3148272 ops/sec; (177099990 of 177099990 found)
+ # V1: overwrite : 7.939 micros/op 125963 ops/sec; 50.5 MB/s
+ # V2: overwrite : 7.854 micros/op 127320 ops/sec 1800.001 seconds 229176999 operations; 51.0 MB/s
+
+ format_version=$( grep ^"$bench_name" "$test_out" \
+ | awk '{ if (NF >= 10 && $8 == "seconds") { print "V2" } else { print "V1" } }' )
+ if [ $format_version == "V1" ]; then
+ ops_sec=$( grep ^"$bench_name" "$test_out" | awk '{ print $5 }' )
+ usecs_op=$( grep ^"$bench_name" "$test_out" | awk '{ printf "%.1f", $3 }' )
+ if [ "$bench_name" == "multireadrandom" ]; then
+ mb_sec="NA"
+ else
+ mb_sec=$( grep ^"$bench_name" "$test_out" | awk '{ print $7 }' )
+ fi
+ else
+ ops_sec=$( grep ^"$bench_name" "$test_out" | awk '{ print $5 }' )
+ usecs_op=$( grep ^"$bench_name" "$test_out" | awk '{ printf "%.1f", $3 }' )
+ mb_sec=$( grep ^"$bench_name" "$test_out" | awk '{ print $11 }' )
+ fi
+
+ # For RocksDB version 4.x there are fewer fields but this still parses correctly
+ # Cumulative writes: 242M writes, 242M keys, 18M commit groups, 12.9 writes per commit group, ingest: 95.96 GB, 54.69 MB/s
+ cum_writes_gb_orig=$( grep "^Cumulative writes" "$test_out" | tail -1 | awk '{ for (x=1; x<=NF; x++) { if ($x == "ingest:") { printf "%.1f", $(x+1) } } }' )
+ cum_writes_units=$( grep "^Cumulative writes" "$test_out" | tail -1 | awk '{ for (x=1; x<=NF; x++) { if ($x == "ingest:") { print $(x+2) } } }' | sed 's/,//g' )
+ cum_writes_gb=$( units_as_gb "$cum_writes_gb_orig" "$cum_writes_units" )
+
+ # Cumulative compaction: 1159.74 GB write, 661.03 MB/s write, 1108.89 GB read, 632.04 MB/s read, 6284.3 seconds
+ cmb_ps=$( grep "^Cumulative compaction" "$test_out" | tail -1 | awk '{ printf "%.1f", $6 }' )
+ sum_wgb_orig=$( grep "^Cumulative compaction" "$test_out" | tail -1 | awk '{ printf "%.1f", $3 }' )
+ sum_wgb_units=$( grep "^Cumulative compaction" "$test_out" | tail -1 | awk '{ print $4 }' )
+ sum_wgb=$( units_as_gb "$sum_wgb_orig" "$sum_wgb_units" )
+
+ # Flush(GB): cumulative 97.193, interval 1.247
+ flush_wgb=$( grep "^Flush(GB)" "$test_out" | tail -1 | awk '{ print $3 }' | tr ',' ' ' | awk '{ print $1 }' )
+
+ if [[ "$sum_wgb" == "NA" || \
+ "$cum_writes_gb" == "NA" || \
+ "$cum_writes_gb_orig" == "0.0" || \
+ -z "$cum_writes_gb_orig" || \
+ -z "$flush_wgb" ]]; then
+ wamp="NA"
+ else
+ wamp=$( echo "( $sum_wgb + $flush_wgb ) / $cum_writes_gb" | bc -l | awk '{ printf "%.1f", $1 }' )
+ fi
+
+ c_wsecs=$( grep "^ Sum" $test_out | tail -1 | awk '{ printf "%.0f", $15 }' )
+ c_csecs=$( grep "^ Sum" $test_out | tail -1 | awk '{ printf "%.0f", $16 }' )
+
+ lsm_size=$( grep "^ Sum" "$test_out" | tail -1 | awk '{ printf "%.0f%s", $3, $4 }' )
+ blob_size=$( grep "^Blob file count:" "$test_out" | tail -1 | awk '{ printf "%.0f%s", $7, $8 }' )
+ # Remove the trailing comma from blob_size: 3.0GB, -> 3.0GB
+ blob_size="${blob_size/,/}"
+
+ b_rgb=$( grep "^ Sum" $test_out | tail -1 | awk '{ printf "%.0f", $21 }' )
+ b_wgb=$( grep "^ Sum" $test_out | tail -1 | awk '{ printf "%.0f", $22 }' )
+
+ p50=$( grep "^Percentiles:" $test_out | tail -1 | awk '{ printf "%.1f", $3 }' )
+ p99=$( grep "^Percentiles:" $test_out | tail -1 | awk '{ printf "%.0f", $7 }' )
+ p999=$( grep "^Percentiles:" $test_out | tail -1 | awk '{ printf "%.0f", $9 }' )
+ p9999=$( grep "^Percentiles:" $test_out | tail -1 | awk '{ printf "%.0f", $11 }' )
+ pmax=$( grep "^Min: " $test_out | grep Median: | grep Max: | awk '{ printf "%.0f", $6 }' )
+
+ # Use the last line because there might be extra lines when the db_bench process exits with an error
+ time_out="$test_out".time
+ u_cpu=$( tail -1 "$time_out" | awk '{ printf "%.1f", $2 / 1000.0 }' )
+ s_cpu=$( tail -1 "$time_out" | awk '{ printf "%.1f", $3 / 1000.0 }' )
+
+ rss="NA"
+ if [ -f $test_out.stats.ps ]; then
+ rss=$( awk '{ printf "%.1f\n", $6 / (1024 * 1024) }' "$test_out".stats.ps | sort -n | tail -1 )
+ fi
+
+ # if the report TSV (Tab Separate Values) file does not yet exist, create it and write the header row to it
+ if [ ! -f "$report" ]; then
+ echo -e "# ops_sec - operations per second" >> "$report"
+ echo -e "# mb_sec - ops_sec * size-of-operation-in-MB" >> "$report"
+ echo -e "# lsm_sz - size of LSM tree" >> "$report"
+ echo -e "# blob_sz - size of BlobDB logs" >> "$report"
+ echo -e "# c_wgb - GB written by compaction" >> "$report"
+ echo -e "# w_amp - Write-amplification as (bytes written by compaction / bytes written by memtable flush)" >> "$report"
+ echo -e "# c_mbps - Average write rate for compaction" >> "$report"
+ echo -e "# c_wsecs - Wall clock seconds doing compaction" >> "$report"
+ echo -e "# c_csecs - CPU seconds doing compaction" >> "$report"
+ echo -e "# b_rgb - Blob compaction read GB" >> "$report"
+ echo -e "# b_wgb - Blob compaction write GB" >> "$report"
+ echo -e "# usec_op - Microseconds per operation" >> "$report"
+ echo -e "# p50, p99, p99.9, p99.99 - 50th, 99th, 99.9th, 99.99th percentile response time in usecs" >> "$report"
+ echo -e "# pmax - max response time in usecs" >> "$report"
+ echo -e "# uptime - RocksDB uptime in seconds" >> "$report"
+ echo -e "# stall% - Percentage of time writes are stalled" >> "$report"
+ echo -e "# Nstall - Number of stalls" >> "$report"
+ echo -e "# u_cpu - #seconds/1000 of user CPU" >> "$report"
+ echo -e "# s_cpu - #seconds/1000 of system CPU" >> "$report"
+ echo -e "# rss - max RSS in GB for db_bench process" >> "$report"
+ echo -e "# test - Name of test" >> "$report"
+ echo -e "# date - Date/time of test" >> "$report"
+ echo -e "# version - RocksDB version" >> "$report"
+ echo -e "# job_id - User-provided job ID" >> "$report"
+ echo -e "# githash - git hash at which db_bench was compiled" >> "$report"
+ echo -e $tsv_header >> "$report"
+ fi
+
+ echo -e "$ops_sec\t$mb_sec\t$lsm_size\t$blob_size\t$sum_wgb\t$wamp\t$cmb_ps\t$c_wsecs\t$c_csecs\t$b_rgb\t$b_wgb\t$usecs_op\t$p50\t$p99\t$p999\t$p9999\t$pmax\t$uptime\t$stall_pct\t$nstall\t$u_cpu\t$s_cpu\t$rss\t$test_name\t$my_date\t$version\t$job_id\t$git_hash" \
+ >> "$report"
+}
+
+function run_bulkload {
+ # This runs with a vector memtable and the WAL disabled to load faster. It is still crash safe and the
+ # client can discover where to restart a load after a crash. I think this is a good way to load.
+ echo "Bulk loading $num_keys random keys"
+ log_file_name=$output_dir/benchmark_bulkload_fillrandom.log
+ time_cmd=$( get_cmd $log_file_name.time )
+ cmd="$time_cmd ./db_bench --benchmarks=fillrandom,stats \
+ --use_existing_db=0 \
+ --disable_auto_compactions=1 \
+ --sync=0 \
+ $params_bulkload \
+ --threads=1 \
+ --memtablerep=vector \
+ --allow_concurrent_memtable_write=false \
+ --disable_wal=1 \
+ --seed=$( date +%s ) \
+ --report_file=${log_file_name}.r.csv \
+ 2>&1 | tee -a $log_file_name"
+ if [[ "$job_id" != "" ]]; then
+ echo "Job ID: ${job_id}" > $log_file_name
+ echo $cmd | tee -a $log_file_name
+ else
+ echo $cmd | tee $log_file_name
+ fi
+ eval $cmd
+ summarize_result $log_file_name bulkload fillrandom
+
+ echo "Compacting..."
+ log_file_name=$output_dir/benchmark_bulkload_compact.log
+ time_cmd=$( get_cmd $log_file_name.time )
+ cmd="$time_cmd ./db_bench --benchmarks=compact,stats \
+ --use_existing_db=1 \
+ --disable_auto_compactions=1 \
+ --sync=0 \
+ $params_w \
+ --threads=1 \
+ 2>&1 | tee -a $log_file_name"
+ if [[ "$job_id" != "" ]]; then
+ echo "Job ID: ${job_id}" > $log_file_name
+ echo $cmd | tee -a $log_file_name
+ else
+ echo $cmd | tee $log_file_name
+ fi
+ eval $cmd
+}
+
+#
+# Parameter description:
+#
+# $1 - 1 if I/O statistics should be collected.
+# $2 - compaction type to use (level=0, universal=1).
+# $3 - number of subcompactions.
+# $4 - number of maximum background compactions.
+#
+function run_manual_compaction_worker {
+ # This runs with a vector memtable and the WAL disabled to load faster.
+ # It is still crash safe and the client can discover where to restart a
+ # load after a crash. I think this is a good way to load.
+ echo "Bulk loading $num_keys random keys for manual compaction."
+
+ log_file_name=$output_dir/benchmark_man_compact_fillrandom_$3.log
+
+ if [ "$2" == "1" ]; then
+ extra_params=$params_univ_compact
+ else
+ extra_params=$params_level_compact
+ fi
+
+ # Make sure that fillrandom uses the same compaction options as compact.
+ time_cmd=$( get_cmd $log_file_name.time )
+ cmd="$time_cmd ./db_bench --benchmarks=fillrandom,stats \
+ --use_existing_db=0 \
+ --disable_auto_compactions=0 \
+ --sync=0 \
+ $extra_params \
+ --threads=$num_threads \
+ --compaction_measure_io_stats=$1 \
+ --compaction_style=$2 \
+ --subcompactions=$3 \
+ --memtablerep=vector \
+ --allow_concurrent_memtable_write=false \
+ --disable_wal=1 \
+ --max_background_compactions=$4 \
+ --seed=$( date +%s ) \
+ 2>&1 | tee -a $log_file_name"
+
+ if [[ "$job_id" != "" ]]; then
+ echo "Job ID: ${job_id}" > $log_file_name
+ echo $cmd | tee -a $log_file_name
+ else
+ echo $cmd | tee $log_file_name
+ fi
+ eval $cmd
+
+ summarize_result $log_file_namefillrandom_output_file man_compact_fillrandom_$3 fillrandom
+
+ echo "Compacting with $3 subcompactions specified ..."
+
+ log_file_name=$output_dir/benchmark_man_compact_$3.log
+
+ # This is the part we're really interested in. Given that compact benchmark
+ # doesn't output regular statistics then we'll just use the time command to
+ # measure how long this step takes.
+ cmd="{ \
+ time ./db_bench --benchmarks=compact,stats \
+ --use_existing_db=1 \
+ --disable_auto_compactions=0 \
+ --sync=0 \
+ $extra_params \
+ --threads=$num_threads \
+ --compaction_measure_io_stats=$1 \
+ --compaction_style=$2 \
+ --subcompactions=$3 \
+ --max_background_compactions=$4 \
+ ;}
+ 2>&1 | tee -a $log_file_name"
+
+ if [[ "$job_id" != "" ]]; then
+ echo "Job ID: ${job_id}" > $log_file_name
+ echo $cmd | tee -a $log_file_name
+ else
+ echo $cmd | tee $log_file_name
+ fi
+ eval $cmd
+
+ # Can't use summarize_result here. One way to analyze the results is to run
+ # "grep real" on the resulting log files.
+}
+
+function run_univ_compaction {
+ # Always ask for I/O statistics to be measured.
+ io_stats=1
+
+ # Values: kCompactionStyleLevel = 0x0, kCompactionStyleUniversal = 0x1.
+ compaction_style=1
+
+ # Define a set of benchmarks.
+ subcompactions=(1 2 4 8 16)
+ max_background_compactions=(16 16 8 4 2)
+
+ i=0
+ total=${#subcompactions[@]}
+
+ # Execute a set of benchmarks to cover variety of scenarios.
+ while [ "$i" -lt "$total" ]
+ do
+ run_manual_compaction_worker $io_stats $compaction_style ${subcompactions[$i]} \
+ ${max_background_compactions[$i]}
+ ((i++))
+ done
+}
+
+function run_fillseq {
+ # This runs with a vector memtable. WAL can be either disabled or enabled
+ # depending on the input parameter (1 for disabled, 0 for enabled). The main
+ # benefit behind disabling WAL is to make loading faster. It is still crash
+ # safe and the client can discover where to restart a load after a crash. I
+ # think this is a good way to load.
+
+ # Make sure that we'll have unique names for all the files so that data won't
+ # be overwritten.
+ if [ $1 == 1 ]; then
+ log_file_name="${output_dir}/benchmark_fillseq.wal_disabled.v${value_size}.log"
+ test_name=fillseq.wal_disabled.v${value_size}
+ else
+ log_file_name="${output_dir}/benchmark_fillseq.wal_enabled.v${value_size}.log"
+ test_name=fillseq.wal_enabled.v${value_size}
+ fi
+
+ # For Leveled compaction hardwire this to 0 so that data that is trivial-moved
+ # to larger levels (3, 4, etc) will be compressed.
+ if [ $compaction_style == "leveled" ]; then
+ comp_arg="--min_level_to_compress=0"
+ elif [ $compaction_style == "universal" ]; then
+ if [ ! -z $UNIVERSAL_ALLOW_TRIVIAL_MOVE ]; then
+ # See GetCompressionFlush where compression_size_percent < 1 means use the default
+ # compression which is needed because trivial moves are enabled
+ comp_arg="--universal_compression_size_percent=-1"
+ else
+ # See GetCompressionFlush where compression_size_percent > 0 means no compression.
+ # Don't set anything here because compression_size_percent is set in univ_const_params
+ comp_arg=""
+ fi
+ else
+ # compaction_style == "blob"
+ comp_arg="--min_level_to_compress=0"
+ fi
+
+ echo "Loading $num_keys keys sequentially"
+ time_cmd=$( get_cmd $log_file_name.time )
+ cmd="$time_cmd ./db_bench --benchmarks=fillseq,stats \
+ $params_fillseq \
+ $comp_arg \
+ --use_existing_db=0 \
+ --sync=0 \
+ --threads=1 \
+ --memtablerep=vector \
+ --allow_concurrent_memtable_write=false \
+ --disable_wal=$1 \
+ --seed=$( date +%s ) \
+ --report_file=${log_file_name}.r.csv \
+ 2>&1 | tee -a $log_file_name"
+ if [[ "$job_id" != "" ]]; then
+ echo "Job ID: ${job_id}" > $log_file_name
+ echo $cmd | tee -a $log_file_name
+ else
+ echo $cmd | tee $log_file_name
+ fi
+ start_stats $log_file_name.stats
+ eval $cmd
+ stop_stats $log_file_name.stats
+
+ # The constant "fillseq" which we pass to db_bench is the benchmark name.
+ summarize_result $log_file_name $test_name fillseq
+}
+
+function run_lsm {
+ # This flushes the memtable and L0 to get the LSM tree into a deterministic
+ # state for read-only tests that will follow.
+ echo "Flush memtable, wait, compact L0, wait"
+ job=$1
+
+ if [ $job = flush_mt_l0 ]; then
+ benchmarks=levelstats,flush,waitforcompaction,compact0,waitforcompaction,memstats,levelstats
+ elif [ $job = waitforcompaction ]; then
+ benchmarks=levelstats,waitforcompaction,memstats,levelstats
+ else
+ echo Job unknown: $job
+ exit $EXIT_NOT_COMPACTION_TEST
+ fi
+
+ log_file_name=$output_dir/benchmark_${job}.log
+ time_cmd=$( get_cmd $log_file_name.time )
+ cmd="$time_cmd ./db_bench --benchmarks=$benchmarks,stats \
+ --use_existing_db=1 \
+ --sync=0 \
+ $params_w \
+ --threads=1 \
+ --seed=$( date +%s ) \
+ 2>&1 | tee -a $log_file_name"
+ if [[ "$job_id" != "" ]]; then
+ echo "Job ID: ${job_id}" > $log_file_name
+ echo $cmd | tee -a $log_file_name
+ else
+ echo $cmd | tee $log_file_name
+ fi
+ start_stats $log_file_name.stats
+ # waitforcompaction can hang with universal (compaction_style=1)
+ # see bug https://github.com/facebook/rocksdb/issues/9275
+ eval $cmd
+ stop_stats $log_file_name.stats
+ # Don't summarize, the log doesn't have the output needed for it
+}
+
+function run_change {
+ output_name=$1
+ grep_name=$2
+ benchmarks=$3
+ echo "Do $num_keys random $output_name"
+ log_file_name="$output_dir/benchmark_${output_name}.t${num_threads}.s${syncval}.log"
+ time_cmd=$( get_cmd $log_file_name.time )
+ cmd="$time_cmd ./db_bench --benchmarks=$benchmarks,stats \
+ --use_existing_db=1 \
+ --sync=$syncval \
+ $params_w \
+ --threads=$num_threads \
+ --merge_operator=\"put\" \
+ --seed=$( date +%s ) \
+ --report_file=${log_file_name}.r.csv \
+ 2>&1 | tee -a $log_file_name"
+ if [[ "$job_id" != "" ]]; then
+ echo "Job ID: ${job_id}" > $log_file_name
+ echo $cmd | tee -a $log_file_name
+ else
+ echo $cmd | tee $log_file_name
+ fi
+ start_stats $log_file_name.stats
+ eval $cmd
+ stop_stats $log_file_name.stats
+ summarize_result $log_file_name ${output_name}.t${num_threads}.s${syncval} $grep_name
+}
+
+function run_filluniquerandom {
+ echo "Loading $num_keys unique keys randomly"
+ log_file_name=$output_dir/benchmark_filluniquerandom.log
+ time_cmd=$( get_cmd $log_file_name.time )
+ cmd="$time_cmd ./db_bench --benchmarks=filluniquerandom,stats \
+ --use_existing_db=0 \
+ --sync=0 \
+ $params_w \
+ --threads=1 \
+ --seed=$( date +%s ) \
+ --report_file=${log_file_name}.r.csv \
+ 2>&1 | tee -a $log_file_name"
+ if [[ "$job_id" != "" ]]; then
+ echo "Job ID: ${job_id}" > $log_file_name
+ echo $cmd | tee -a $log_file_name
+ else
+ echo $cmd | tee $log_file_name
+ fi
+ start_stats $log_file_name.stats
+ eval $cmd
+ stop_stats $log_file_name.stats
+ summarize_result $log_file_name filluniquerandom filluniquerandom
+}
+
+function run_readrandom {
+ echo "Reading $num_keys random keys"
+ log_file_name="${output_dir}/benchmark_readrandom.t${num_threads}.log"
+ time_cmd=$( get_cmd $log_file_name.time )
+ cmd="$time_cmd ./db_bench --benchmarks=readrandom,stats \
+ --use_existing_db=1 \
+ $params_w \
+ --threads=$num_threads \
+ --seed=$( date +%s ) \
+ --report_file=${log_file_name}.r.csv \
+ 2>&1 | tee -a $log_file_name"
+ if [[ "$job_id" != "" ]]; then
+ echo "Job ID: ${job_id}" > $log_file_name
+ echo $cmd | tee -a $log_file_name
+ else
+ echo $cmd | tee $log_file_name
+ fi
+ start_stats $log_file_name.stats
+ eval $cmd
+ stop_stats $log_file_name.stats
+ summarize_result $log_file_name readrandom.t${num_threads} readrandom
+}
+
+function run_multireadrandom {
+ echo "Multi-Reading $num_keys random keys"
+ log_file_name="${output_dir}/benchmark_multireadrandom.t${num_threads}.log"
+ time_cmd=$( get_cmd $log_file_name.time )
+ cmd="$time_cmd ./db_bench --benchmarks=multireadrandom,stats \
+ --use_existing_db=1 \
+ --threads=$num_threads \
+ --batch_size=10 \
+ $params_w \
+ --seed=$( date +%s ) \
+ --report_file=${log_file_name}.r.csv \
+ 2>&1 | tee -a $log_file_name"
+ if [[ "$job_id" != "" ]]; then
+ echo "Job ID: ${job_id}" > $log_file_name
+ echo $cmd | tee -a $log_file_name
+ else
+ echo $cmd | tee $log_file_name
+ fi
+ start_stats $log_file_name.stats
+ eval $cmd
+ stop_stats $log_file_name.stats
+ summarize_result $log_file_name multireadrandom.t${num_threads} multireadrandom
+}
+
+function run_readwhile {
+ operation=$1
+ echo "Reading $num_keys random keys while $operation"
+ log_file_name="${output_dir}/benchmark_readwhile${operation}.t${num_threads}.log"
+ time_cmd=$( get_cmd $log_file_name.time )
+ cmd="$time_cmd ./db_bench --benchmarks=readwhile${operation},stats \
+ --use_existing_db=1 \
+ --sync=$syncval \
+ $params_w \
+ --threads=$num_threads \
+ --merge_operator=\"put\" \
+ --seed=$( date +%s ) \
+ --report_file=${log_file_name}.r.csv \
+ 2>&1 | tee -a $log_file_name"
+ if [[ "$job_id" != "" ]]; then
+ echo "Job ID: ${job_id}" > $log_file_name
+ echo $cmd | tee -a $log_file_name
+ else
+ echo $cmd | tee $log_file_name
+ fi
+ start_stats $log_file_name.stats
+ eval $cmd
+ stop_stats $log_file_name.stats
+ summarize_result $log_file_name readwhile${operation}.t${num_threads} readwhile${operation}
+}
+
+function run_rangewhile {
+ operation=$1
+ full_name=$2
+ reverse_arg=$3
+ log_file_name="${output_dir}/benchmark_${full_name}.t${num_threads}.log"
+ time_cmd=$( get_cmd $log_file_name.time )
+ echo "Range scan $num_keys random keys while ${operation} for reverse_iter=${reverse_arg}"
+ cmd="$time_cmd ./db_bench --benchmarks=seekrandomwhile${operation},stats \
+ --use_existing_db=1 \
+ --sync=$syncval \
+ $params_w \
+ --threads=$num_threads \
+ --merge_operator=\"put\" \
+ --seek_nexts=$num_nexts_per_seek \
+ --reverse_iterator=$reverse_arg \
+ --seed=$( date +%s ) \
+ --report_file=${log_file_name}.r.csv \
+ 2>&1 | tee -a $log_file_name"
+ echo $cmd | tee $log_file_name
+ start_stats $log_file_name.stats
+ eval $cmd
+ stop_stats $log_file_name.stats
+ summarize_result $log_file_name ${full_name}.t${num_threads} seekrandomwhile${operation}
+}
+
+function run_range {
+ full_name=$1
+ reverse_arg=$2
+ log_file_name="${output_dir}/benchmark_${full_name}.t${num_threads}.log"
+ time_cmd=$( get_cmd $log_file_name.time )
+ echo "Range scan $num_keys random keys for reverse_iter=${reverse_arg}"
+ cmd="$time_cmd ./db_bench --benchmarks=seekrandom,stats \
+ --use_existing_db=1 \
+ $params_w \
+ --threads=$num_threads \
+ --seek_nexts=$num_nexts_per_seek \
+ --reverse_iterator=$reverse_arg \
+ --seed=$( date +%s ) \
+ --report_file=${log_file_name}.r.csv \
+ 2>&1 | tee -a $log_file_name"
+ if [[ "$job_id" != "" ]]; then
+ echo "Job ID: ${job_id}" > $log_file_name
+ echo $cmd | tee -a $log_file_name
+ else
+ echo $cmd | tee $log_file_name
+ fi
+ start_stats $log_file_name.stats
+ eval $cmd
+ stop_stats $log_file_name.stats
+ summarize_result $log_file_name ${full_name}.t${num_threads} seekrandom
+}
+
+function run_randomtransaction {
+ echo "..."
+ log_file_name=$output_dir/benchmark_randomtransaction.log
+ time_cmd=$( get_cmd $log_file_name.time )
+ cmd="$time_cmd ./db_bench $params_w --benchmarks=randomtransaction,stats \
+ --num=$num_keys \
+ --transaction_db \
+ --threads=5 \
+ --transaction_sets=5 \
+ --report_file=${log_file_name}.r.csv \
+ 2>&1 | tee $log_file_name"
+ if [[ "$job_id" != "" ]]; then
+ echo "Job ID: ${job_id}" > $log_file_name
+ echo $cmd | tee -a $log_file_name
+ else
+ echo $cmd | tee $log_file_name
+ fi
+ start_stats $log_file_name.stats
+ eval $cmd
+ stop_stats $log_file_name.stats
+}
+
+function now() {
+ echo `date +"%s"`
+}
+
+
+echo "===== Benchmark ====="
+
+# Run!!!
+IFS=',' read -a jobs <<< $bench_cmd
+# shellcheck disable=SC2068
+for job in ${jobs[@]}; do
+
+ if [ $job != debug ]; then
+ echo "Starting $job (ID: $job_id) at `date`" | tee -a $schedule
+ fi
+
+ start=$(now)
+ if [ $job = bulkload ]; then
+ run_bulkload
+ elif [ $job = flush_mt_l0 ]; then
+ run_lsm flush_mt_l0
+ elif [ $job = waitforcompaction ]; then
+ run_lsm waitforcompaction
+ elif [ $job = fillseq_disable_wal ]; then
+ run_fillseq 1
+ elif [ $job = fillseq_enable_wal ]; then
+ run_fillseq 0
+ elif [ $job = overwrite ]; then
+ run_change overwrite overwrite overwrite
+ elif [ $job = overwritesome ]; then
+ # This uses a different name for overwrite results so it can be run twice in one benchmark run.
+ run_change overwritesome overwrite overwrite
+ elif [ $job = overwriteandwait ]; then
+ run_change overwriteandwait overwrite overwrite,waitforcompaction
+ elif [ $job = updaterandom ]; then
+ run_change updaterandom updaterandom updaterandom
+ elif [ $job = mergerandom ]; then
+ run_change mergerandom mergerandom mergerandom
+ elif [ $job = filluniquerandom ]; then
+ run_filluniquerandom
+ elif [ $job = readrandom ]; then
+ run_readrandom
+ elif [ $job = multireadrandom ]; then
+ run_multireadrandom
+ elif [ $job = fwdrange ]; then
+ run_range $job false
+ elif [ $job = revrange ]; then
+ run_range $job true
+ elif [ $job = readwhilewriting ]; then
+ run_readwhile writing
+ elif [ $job = readwhilemerging ]; then
+ run_readwhile merging
+ elif [ $job = fwdrangewhilewriting ]; then
+ run_rangewhile writing $job false
+ elif [ $job = revrangewhilewriting ]; then
+ run_rangewhile writing $job true
+ elif [ $job = fwdrangewhilemerging ]; then
+ run_rangewhile merging $job false
+ elif [ $job = revrangewhilemerging ]; then
+ run_rangewhile merging $job true
+ elif [ $job = randomtransaction ]; then
+ run_randomtransaction
+ elif [ $job = universal_compaction ]; then
+ run_univ_compaction
+ elif [ $job = debug ]; then
+ num_keys=1000; # debug
+ echo "Setting num_keys to $num_keys"
+ else
+ echo "unknown job $job"
+ exit $EXIT_UNKNOWN_JOB
+ fi
+ end=$(now)
+
+ if [ $job != debug ]; then
+ echo "Completed $job (ID: $job_id) in $((end-start)) seconds" | tee -a $schedule
+ fi
+
+ echo -e $tsv_header
+ tail -1 $report
+
+done
diff --git a/src/rocksdb/tools/benchmark_ci.py b/src/rocksdb/tools/benchmark_ci.py
new file mode 100755
index 000000000..de9f69cf9
--- /dev/null
+++ b/src/rocksdb/tools/benchmark_ci.py
@@ -0,0 +1,182 @@
+#!/usr/bin/env python3
+# Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+# This source code is licensed under both the GPLv2 (found in the
+# COPYING file in the root directory) and Apache 2.0 License
+# (found in the LICENSE.Apache file in the root directory).
+
+"""Run benchmark_compare.sh on the most recent build, for CI
+"""
+
+import argparse
+import glob
+import logging
+import os
+import re
+import shutil
+import subprocess
+import sys
+
+logging.basicConfig(level=logging.INFO)
+
+
+class Config:
+ def __init__(self, args):
+ self.version_file = "./include/rocksdb/version.h"
+ self.data_dir = os.path.expanduser(f"{args.db_dir}")
+ self.results_dir = os.path.expanduser(f"{args.output_dir}")
+ self.benchmark_script = f"{os.getcwd()}/tools/benchmark_compare.sh"
+ self.benchmark_cwd = f"{os.getcwd()}/tools"
+
+ benchmark_env_keys = [
+ "LD_LIBRARY_PATH",
+ "NUM_KEYS",
+ "KEY_SIZE",
+ "VALUE_SIZE",
+ "CACHE_SIZE_MB",
+ "DURATION_RW",
+ "DURATION_RO",
+ "MB_WRITE_PER_SEC",
+ "NUM_THREADS",
+ "COMPRESSION_TYPE",
+ "MIN_LEVEL_TO_COMPRESS",
+ "WRITE_BUFFER_SIZE_MB",
+ "TARGET_FILE_SIZE_BASE_MB",
+ "MAX_BYTES_FOR_LEVEL_BASE_MB",
+ "MAX_BACKGROUND_JOBS",
+ "CACHE_INDEX_AND_FILTER_BLOCKS",
+ "USE_O_DIRECT",
+ "STATS_INTERVAL_SECONDS",
+ "SUBCOMPACTIONS",
+ "COMPACTION_STYLE",
+ "CI_TESTS_ONLY",
+ ]
+
+
+def read_version(config):
+ majorRegex = re.compile(r"#define ROCKSDB_MAJOR\s([0-9]+)")
+ minorRegex = re.compile(r"#define ROCKSDB_MINOR\s([0-9]+)")
+ patchRegex = re.compile(r"#define ROCKSDB_PATCH\s([0-9]+)")
+ with open(config.version_file, "r") as reader:
+ major = None
+ minor = None
+ patch = None
+ for line in reader:
+ if major is None:
+ major = majorRegex.match(line)
+ elif minor is None:
+ minor = minorRegex.match(line)
+ elif patch is None:
+ patch = patchRegex.match(line)
+
+ if patch is not None:
+ break
+
+ if patch is not None:
+ return (major.group(1), minor.group(1), patch.group(1))
+
+ # Didn't complete a match
+ return None
+
+
+def prepare(version_str, config):
+ old_files = glob.glob(f"{config.results_dir}/{version_str}/**", recursive=True)
+ for f in old_files:
+ if os.path.isfile(f):
+ logging.debug(f"remove file {f}")
+ os.remove(f)
+ for f in old_files:
+ if os.path.isdir(f):
+ logging.debug(f"remove dir {f}")
+ os.rmdir(f)
+
+ db_bench_vers = f"{config.benchmark_cwd}/db_bench.{version_str}"
+
+ # Create a symlink to the db_bench executable
+ os.symlink(f"{os.getcwd()}/db_bench", db_bench_vers)
+
+
+def results(version_str, config):
+ # Copy the report TSV file back to the top level of results
+ shutil.copyfile(
+ f"{config.results_dir}/{version_str}/report.tsv",
+ f"{config.results_dir}/report.tsv",
+ )
+
+
+def cleanup(version_str, config):
+ # Remove the symlink to the db_bench executable
+ db_bench_vers = f"{config.benchmark_cwd}/db_bench.{version_str}"
+ os.remove(db_bench_vers)
+
+
+def get_benchmark_env():
+ env = []
+ for key in Config.benchmark_env_keys:
+ value = os.getenv(key)
+ if value is not None:
+ env.append((key, value))
+ return env
+
+
+def main():
+ """Tool for running benchmark_compare.sh on the most recent build, for CI
+ This tool will
+
+ (1) Work out the current version of RocksDB
+ (2) Run benchmark_compare with that version alone
+ """
+
+ parser = argparse.ArgumentParser(
+ description="benchmark_compare.sh Python wrapper for CI."
+ )
+
+ # --tsvfile is the name of the file to read results from
+ # --esdocument is the ElasticSearch document to push these results into
+ #
+ parser.add_argument(
+ "--db_dir",
+ default="~/tmp/rocksdb-benchmark-datadir",
+ help="Database directory hierarchy to use",
+ )
+ parser.add_argument(
+ "--output_dir",
+ default="~/tmp/benchmark-results",
+ help="Benchmark output goes here",
+ )
+ parser.add_argument(
+ "--num_keys",
+ default="10000",
+ help="Number of database keys to use in benchmark test(s) (determines size of test job)",
+ )
+ args = parser.parse_args()
+ config = Config(args)
+
+ version = read_version(config)
+ if version is None:
+ raise Exception(f"Could not read RocksDB version from {config.version_file}")
+ version_str = f"{version[0]}.{version[1]}.{version[2]}"
+ logging.info(f"Run benchmark_ci with RocksDB version {version_str}")
+
+ prepare(version_str, config)
+
+ try:
+ env = get_benchmark_env()
+ env.append(("NUM_KEYS", args.num_keys))
+ cmd = [
+ config.benchmark_script,
+ config.data_dir,
+ config.results_dir,
+ version_str,
+ ]
+ logging.info(f"Run {cmd} env={env} cwd={config.benchmark_cwd}")
+ subprocess.run(cmd, env=dict(env), cwd=config.benchmark_cwd)
+
+ results(version_str, config)
+ finally:
+ cleanup(version_str, config)
+
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/src/rocksdb/tools/benchmark_compare.sh b/src/rocksdb/tools/benchmark_compare.sh
new file mode 100755
index 000000000..ef7990279
--- /dev/null
+++ b/src/rocksdb/tools/benchmark_compare.sh
@@ -0,0 +1,342 @@
+#!/usr/bin/env bash
+# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
+# REQUIRE: db_bench binary exists in the current directory
+
+dbdir=$1
+odir=$2
+
+# Size Constants
+K=1024
+M=$((1024 * K))
+
+# Dynamic loader configuration
+ld_library_path=${LD_LIBRARY_PATH:-""}
+
+# Benchmark configuration
+duration_rw=${DURATION_RW:-65}
+duration_ro=${DURATION_RO:-65}
+num_keys=${NUM_KEYS:-1000000}
+num_threads=${NUM_THREADS:-16}
+key_size=${KEY_SIZE:-20}
+value_size=${VALUE_SIZE:-400}
+mb_write_per_sec=${MB_WRITE_PER_SEC:-2}
+ci_tests_only=${CI_TESTS_ONLY:-"false"}
+
+# RocksDB configuration
+compression_type=${COMPRESSION_TYPE:-lz4}
+subcompactions=${SUBCOMPACTIONS:-1}
+write_buffer_size_mb=${WRITE_BUFFER_SIZE_MB:-32}
+target_file_size_base_mb=${TARGET_FILE_SIZE_BASE_MB:-32}
+max_bytes_for_level_base_mb=${MAX_BYTES_FOR_LEVEL_BASE_MB:-128}
+max_background_jobs=${MAX_BACKGROUND_JOBS:-8}
+stats_interval_seconds=${STATS_INTERVAL_SECONDS:-20}
+cache_index_and_filter_blocks=${CACHE_INDEX_AND_FILTER_BLOCKS:-0}
+# USE_O_DIRECT doesn't need a default
+bytes_per_sync=${BYTES_PER_SYNC:-$(( 1 * M ))}
+# CACHE_SIZE_MB doesn't need a default
+min_level_to_compress=${MIN_LEVEL_TO_COMPRESS:-"-1"}
+
+compaction_style=${COMPACTION_STYLE:-leveled}
+if [ "$compaction_style" = "leveled" ]; then
+ echo Use leveled compaction
+elif [ "$compaction_style" = "universal" ]; then
+ echo Use universal compaction
+elif [ "$compaction_style" = "blob" ]; then
+ echo Use blob compaction
+else
+ echo COMPACTION_STYLE is :: "$COMPACTION_STYLE" :: and must be one of leveled, universal, blob
+ exit 1
+fi
+
+# Leveled compaction configuration
+level0_file_num_compaction_trigger=${LEVEL0_FILE_NUM_COMPACTION_TRIGGER:-4}
+level0_slowdown_writes_trigger=${LEVEL0_SLOWDOWN_WRITES_TRIGGER:-20}
+level0_stop_writes_trigger=${LEVEL0_STOP_WRITES_TRIGGER:-30}
+per_level_fanout=${PER_LEVEL_FANOUT:-8}
+
+# Universal compaction configuration
+universal_min_merge_width=${UNIVERSAL_MIN_MERGE_WIDTH:-2}
+universal_max_merge_width=${UNIVERSAL_MAX_MERGE_WIDTH:-20}
+universal_size_ratio=${UNIVERSAL_SIZE_RATIO:-1}
+universal_max_size_amp=${UNIVERSAL_MAX_SIZE_AMP:-200}
+universal_compression_size_percent=${UNIVERSAL_COMPRESSION_SIZE_PERCENT:-"-1"}
+
+# Integrated BlobDB configuration
+
+min_blob_size=${MIN_BLOB_SIZE:-0}
+blob_file_size=${BLOB_FILE_SIZE:-$(( 256 * M ))}
+blob_compression_type=${BLOB_COMPRESSION_TYPE:-${compression_type}}
+blob_gc_age_cutoff=${BLOB_GC_AGE_CUTOFF:-"0.25"}
+blob_gc_force_threshold=${BLOB_GC_FORCE_THRESHOLD:-1}
+
+# Arguments for dynamic loading
+base_args=( LD_LIBRARY_PATH="$ld_library_path" )
+
+# Arguments used for all tests
+base_args+=( NUM_KEYS="$num_keys" )
+base_args+=( NUM_THREADS="$num_threads" )
+base_args+=( KEY_SIZE="$key_size" )
+base_args+=( VALUE_SIZE="$value_size" )
+
+base_args+=( SUBCOMPACTIONS="$subcompactions" )
+base_args+=( COMPRESSION_TYPE="$compression_type" )
+base_args+=( WRITE_BUFFER_SIZE_MB="$write_buffer_size_mb" )
+base_args+=( TARGET_FILE_SIZE_BASE_MB="$target_file_size_base_mb" )
+base_args+=( MAX_BYTES_FOR_LEVEL_BASE_MB="$max_bytes_for_level_base_mb" )
+base_args+=( MAX_BACKGROUND_JOBS="$max_background_jobs" )
+base_args+=( STATS_INTERVAL_SECONDS="$stats_interval_seconds" )
+base_args+=( CACHE_INDEX_AND_FILTER_BLOCKS="$cache_index_and_filter_blocks" )
+base_args+=( COMPACTION_STYLE="$compaction_style" )
+base_args+=( BYTES_PER_SYNC="$bytes_per_sync" )
+
+if [ -n "$USE_O_DIRECT" ]; then
+ base_args+=( USE_O_DIRECT=1 )
+fi
+
+if [ -n "$NUMA" ]; then
+ base_args+=( NUMACTL=1 )
+fi
+
+if [ -n "$CACHE_SIZE_MB" ]; then
+ cacheb=$(( CACHE_SIZE_MB * M ))
+ base_args+=( CACHE_SIZE="$cacheb" )
+fi
+
+if [ "$compaction_style" == "leveled" ]; then
+ base_args+=( LEVEL0_FILE_NUM_COMPACTION_TRIGGER="$level0_file_num_compaction_trigger" )
+ base_args+=( LEVEL0_SLOWDOWN_WRITES_TRIGGER="$level0_slowdown_writes_trigger" )
+ base_args+=( LEVEL0_STOP_WRITES_TRIGGER="$level0_stop_writes_trigger" )
+ base_args+=( PER_LEVEL_FANOUT="$per_level_fanout" )
+elif [ "$compaction_style" == "universal" ]; then
+ base_args+=( LEVEL0_FILE_NUM_COMPACTION_TRIGGER="$level0_file_num_compaction_trigger" )
+ base_args+=( LEVEL0_SLOWDOWN_WRITES_TRIGGER="$level0_slowdown_writes_trigger" )
+ base_args+=( LEVEL0_STOP_WRITES_TRIGGER="$level0_stop_writes_trigger" )
+ base_args+=( UNIVERSAL_MIN_MERGE_WIDTH="$universal_min_merge_width" )
+ base_args+=( UNIVERSAL_MAX_MERGE_WIDTH="$universal_max_merge_width" )
+ base_args+=( UNIVERSAL_SIZE_RATIO="$universal_size_ratio" )
+ base_args+=( UNIVERSAL_MAX_SIZE_AMP="$universal_max_size_amp" )
+ if [ -n "$UNIVERSAL_ALLOW_TRIVIAL_MOVE" ]; then
+ base_args+=( UNIVERSAL_ALLOW_TRIVIAL_MOVE=1 )
+ fi
+else
+ # Inherit settings for leveled because index uses leveled LSM
+ base_args+=( LEVEL0_FILE_NUM_COMPACTION_TRIGGER="$level0_file_num_compaction_trigger" )
+ base_args+=( LEVEL0_SLOWDOWN_WRITES_TRIGGER="$level0_slowdown_writes_trigger" )
+ base_args+=( LEVEL0_STOP_WRITES_TRIGGER="$level0_stop_writes_trigger" )
+ base_args+=( PER_LEVEL_FANOUT="$per_level_fanout" )
+ # Then add BlobDB specific settings
+ base_args+=( MIN_BLOB_SIZE="$min_blob_size" )
+ base_args+=( BLOB_FILE_SIZE="$blob_file_size" )
+ base_args+=( BLOB_COMPRESSION_TYPE="$blob_compression_type" )
+ base_args+=( BLOB_GC_AGE_CUTOFF="$blob_gc_age_cutoff" )
+ base_args+=( BLOB_GC_FORCE_THRESHOLD="$blob_gc_force_threshold" )
+fi
+
+function usage {
+ echo "usage: benchmark_compare.sh db_dir output_dir version+"
+ echo -e "\tdb_dir\t\tcreate RocksDB database in this directory"
+ echo -e "\toutput_dir\twrite output from performance tests in this directory"
+ echo -e "\tversion+\tspace separated sequence of RocksDB versions to test."
+ echo -e "\nThis expects that db_bench.\$version exists in \$PWD for each version in the sequence."
+ echo -e "An example value for version+ is 6.23.0 6.24.0"
+ echo ""
+ echo -e "Environment variables for options"
+ echo -e "\tNUM_KEYS\t\t\tnumber of keys to load"
+ echo -e "\tKEY_SIZE\t\t\tsize of key"
+ echo -e "\tVALUE_SIZE\t\t\tsize of value"
+ echo -e "\tCACHE_SIZE_MB\t\t\tsize of block cache in MB"
+ echo -e "\tDURATION_RW\t\t\tnumber of seconds for which each test runs, except for read-only tests"
+ echo -e "\tDURATION_RO\t\t\tnumber of seconds for which each read-only test runs"
+ echo -e "\tMB_WRITE_PER_SEC\t\trate limit for writer that runs concurrent with queries for some tests"
+ echo -e "\tNUM_THREADS\t\t\tnumber of user threads"
+ echo -e "\tCOMPRESSION_TYPE\t\tcompression type (zstd, lz4, none, etc)"
+ echo -e "\tMIN_LEVEL_TO_COMPRESS\t\tmin_level_to_compress for leveled"
+ echo -e "\tWRITE_BUFFER_SIZE_MB\t\tsize of write buffer in MB"
+ echo -e "\tTARGET_FILE_SIZE_BASE_MB\tvalue for target_file_size_base in MB"
+ echo -e "\tMAX_BYTES_FOR_LEVEL_BASE_MB\tvalue for max_bytes_for_level_base in MB"
+ echo -e "\tMAX_BACKGROUND_JOBS\t\tvalue for max_background_jobs"
+ echo -e "\tCACHE_INDEX_AND_FILTER_BLOCKS\tvalue for cache_index_and_filter_blocks"
+ echo -e "\tUSE_O_DIRECT\t\t\tUse O_DIRECT for user reads and compaction"
+ echo -e "\tBYTES_PER_SYNC\t\t\tValue for bytes_per_sync"
+ echo -e "\tSTATS_INTERVAL_SECONDS\t\tvalue for stats_interval_seconds"
+ echo -e "\tSUBCOMPACTIONS\t\t\tvalue for subcompactions"
+ echo -e "\tCOMPACTION_STYLE\t\tCompaction style to use, one of: leveled, universal, blob"
+ echo -e "\tCI_TESTS_ONLY\t\tRun a subset of tests tailored to a CI regression job, one of: true, false (default)"
+ echo ""
+ echo -e "\tOptions specific to leveled compaction:"
+ echo -e "\t\tLEVEL0_FILE_NUM_COMPACTION_TRIGGER\tvalue for level0_file_num_compaction_trigger"
+ echo -e "\t\tLEVEL0_SLOWDOWN_WRITES_TRIGGER\t\tvalue for level0_slowdown_writes_trigger"
+ echo -e "\t\tLEVEL0_STOP_WRITES_TRIGGER\t\tvalue for level0_stop_writes_trigger"
+ echo -e "\t\tPER_LEVEL_FANOUT\t\t\tvalue for max_bytes_for_level_multiplier"
+ echo ""
+ echo -e "\tOptions specific to universal compaction:"
+ echo -e "\t\tSee LEVEL0_*_TRIGGER above"
+ echo -e "\t\tUNIVERSAL_MIN_MERGE_WIDTH\t\tvalue of min_merge_width option for universal"
+ echo -e "\t\tUNIVERSAL_MAX_MERGE_WIDTH\t\tvalue of min_merge_width option for universal"
+ echo -e "\t\tUNIVERSAL_SIZE_RATIO\t\t\tvalue of size_ratio option for universal"
+ echo -e "\t\tUNIVERSAL_MAX_SIZE_AMP\t\t\tmax_size_amplification_percent for universal"
+ echo -e "\t\tUNIVERSAL_ALLOW_TRIVIAL_MOVE\t\tSet allow_trivial_move to true for universal, default is false"
+ echo -e "\t\tUNIVERSAL_COMPRESSION_SIZE_PERCENT\tpercentage of LSM tree that should be compressed"
+ echo ""
+ echo -e "\tOptions for integrated BlobDB:"
+ echo -e "\t\tMIN_BLOB_SIZE\t\t\t\tvalue for min_blob_size"
+ echo -e "\t\tBLOB_FILE_SIZE\t\t\t\tvalue for blob_file_size"
+ echo -e "\t\tBLOB_COMPRESSION_TYPE\t\t\tvalue for blob_compression_type"
+ echo -e "\t\tBLOB_GC_AGE_CUTOFF\t\t\tvalue for blog_garbage_collection_age_cutoff"
+ echo -e "\t\tBLOB_GC_FORCE_THRESHOLD\t\t\tvalue for blog_garbage_collection_force_threshold"
+}
+
+function dump_env {
+ echo "Base args" > "$odir"/args
+ echo "${base_args[@]}" | tr ' ' '\n' >> "$odir"/args
+
+ echo -e "\nOther args" >> "$odir"/args
+ echo -e "dbdir\t$dbdir" >> "$odir"/args
+ echo -e "duration_rw\t$duration_rw" >> "$odir"/args
+ echo -e "duration_ro\t$duration_ro" >> "$odir"/args
+ echo -e "per_level_fanout\t$per_level_fanout" >> "$odir"/args
+
+ echo -e "\nargs_load:" >> "$odir"/args
+ echo "${args_load[@]}" | tr ' ' '\n' >> "$odir"/args
+ echo -e "\nargs_nolim:" >> "$odir"/args
+ echo "${args_nolim[@]}" | tr ' ' '\n' >> "$odir"/args
+ echo -e "\nargs_lim:" >> "$odir"/args
+ echo "${args_lim[@]}" | tr ' ' '\n' >> "$odir"/args
+}
+
+if [ $# -lt 3 ]; then
+ usage
+ echo
+ echo "Need at least 3 arguments"
+ exit 1
+fi
+
+shift 2
+
+mkdir -p "$odir"
+
+echo Test versions: "$@"
+echo Test versions: "$@" >> "$odir"/args
+
+for v in "$@" ; do
+ my_odir="$odir"/"$v"
+
+ if [ -d "$my_odir" ]; then
+ echo Exiting because the output directory exists: "$my_odir"
+ exit 1
+ fi
+
+ args_common=("${base_args[@]}")
+
+ args_common+=( OUTPUT_DIR="$my_odir" DB_DIR="$dbdir" WAL_DIR="$dbdir" DB_BENCH_NO_SYNC=1 )
+
+ if [ "$compaction_style" == "leveled" ]; then
+ args_common+=( MIN_LEVEL_TO_COMPRESS="$min_level_to_compress" )
+ elif [ "$compaction_style" == "universal" ]; then
+ args_common+=( UNIVERSAL=1 COMPRESSION_SIZE_PERCENT="$universal_compression_size_percent" )
+ else
+ args_common+=( MIN_LEVEL_TO_COMPRESS="$min_level_to_compress" )
+ fi
+
+ args_load=("${args_common[@]}")
+
+ args_nolim=("${args_common[@]}")
+
+ args_lim=("${args_nolim[@]}")
+ args_lim+=( MB_WRITE_PER_SEC="$mb_write_per_sec" )
+
+ dump_env
+
+ echo Run benchmark for "$v" at "$( date )" with results at "$my_odir"
+ rm -f db_bench
+ echo ln -s db_bench."$v" db_bench
+ ln -s db_bench."$v" db_bench
+
+ find "$dbdir" -type f -exec rm \{\} \;
+
+ # Load in key order
+ echo env "${args_load[@]}" bash ./benchmark.sh fillseq_disable_wal
+ env -i "${args_load[@]}" bash ./benchmark.sh fillseq_disable_wal
+
+ # Read-only tests. The LSM tree shape is in a deterministic state if trivial move
+ # was used during the load.
+
+ # Add revrange with a fixed duration and hardwired number of keys and threads to give
+ # compaction debt leftover from fillseq a chance at being removed. Not using waitforcompaction
+ # here because it isn't supported on older db_bench versions.
+ env -i "${args_nolim[@]}" DURATION=300 NUM_KEYS=100 NUM_THREADS=1 bash ./benchmark.sh revrange
+ env -i "${args_nolim[@]}" DURATION="$duration_ro" bash ./benchmark.sh readrandom
+
+ # Skipped for CI - a single essentail readrandom is enough to set up for other tests
+ if [ "$ci_tests_only" != "true" ]; then
+ env -i "${args_nolim[@]}" DURATION="$duration_ro" bash ./benchmark.sh fwdrange
+ env -i "${args_lim[@]}" DURATION="$duration_ro" bash ./benchmark.sh multireadrandom --multiread_batched
+ else
+ echo "CI_TESTS_ONLY is set, skipping optional read steps."
+ fi
+
+ # Write 10% of the keys. The goal is to randomize keys prior to Lmax
+ p10=$( echo "$num_keys" "$num_threads" | awk '{ printf "%.0f", $1 / $2 / 10.0 }' )
+ env -i "${args_nolim[@]}" WRITES="$p10" bash ./benchmark.sh overwritesome
+
+ if [ "$compaction_style" == "leveled" ]; then
+ # These are not supported by older versions
+ # Flush memtable & L0 to get LSM tree into deterministic state
+ env -i "${args_nolim[@]}" bash ./benchmark.sh flush_mt_l0
+ elif [ "$compaction_style" == "universal" ]; then
+ # For universal don't compact L0 as can have too many sorted runs
+ # waitforcompaction can hang, see https://github.com/facebook/rocksdb/issues/9275
+ # While this is disabled the test that follows will have more variance from compaction debt.
+ # env -i "${args_nolim[@]}" bash ./benchmark.sh waitforcompaction
+ echo TODO enable when waitforcompaction hang is fixed
+ else
+ # These are not supported by older versions
+ # Flush memtable & L0 to get LSM tree into deterministic state
+ env -i "${args_nolim[@]}" bash ./benchmark.sh flush_mt_l0
+ fi
+
+ # Read-mostly tests with a rate-limited writer
+ env -i "${args_lim[@]}" DURATION="$duration_rw" bash ./benchmark.sh revrangewhilewriting
+ env -i "${args_lim[@]}" DURATION="$duration_rw" bash ./benchmark.sh fwdrangewhilewriting
+ env -i "${args_lim[@]}" DURATION="$duration_rw" bash ./benchmark.sh readwhilewriting
+
+ # Write-only tests
+
+ # This creates much compaction debt which will be a problem for tests added after it.
+ # Also, the compaction stats measured at test end can underestimate write-amp depending
+ # on how much compaction debt is allowed.
+ if [ "$compaction_style" == "leveled" ] && ./db_bench --benchmarks=waitforcompaction ; then
+ # Use waitforcompaction to get more accurate write-amp measurement
+ env -i "${args_nolim[@]}" DURATION="$duration_rw" bash ./benchmark.sh overwriteandwait
+ else
+ # waitforcompaction hangs with universal, see https://github.com/facebook/rocksdb/issues/9275
+ env -i "${args_nolim[@]}" DURATION="$duration_rw" bash ./benchmark.sh overwrite
+ fi
+
+ cp "$dbdir"/LOG* "$my_odir"
+ gzip -9 "$my_odir"/LOG*
+
+done
+
+# Generate a file that groups lines from the same test for all versions
+basev=$1
+nlines=$( awk '/^ops_sec/,/END/' "$odir"/"$basev"/report.tsv | grep -v ops_sec | wc -l )
+hline=$( awk '/^ops_sec/ { print NR }' "$odir"/"$basev"/report.tsv )
+sline=$(( hline + 1 ))
+eline=$(( sline + nlines - 1 ))
+
+sum_file="$odir"/summary.tsv
+
+for v in "$@" ; do
+ echo "$odir"/"$v"/report.tsv
+done >> "$sum_file"
+echo >> "$sum_file"
+
+for x in $( seq "$sline" "$eline" ); do
+ awk '{ if (NR == lno) { print $0 } }' lno="$hline" "$odir"/"$basev"/report.tsv >> "$sum_file"
+ for v in "$@" ; do
+ r="$odir"/"$v"/report.tsv
+ awk '{ if (NR == lno) { print $0 } }' lno="$x" "$r" >> "$sum_file"
+ done
+echo >> "$sum_file"
+done
diff --git a/src/rocksdb/tools/benchmark_leveldb.sh b/src/rocksdb/tools/benchmark_leveldb.sh
new file mode 100755
index 000000000..069b53a9f
--- /dev/null
+++ b/src/rocksdb/tools/benchmark_leveldb.sh
@@ -0,0 +1,187 @@
+#!/usr/bin/env bash
+# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
+# REQUIRE: db_bench binary exists in the current directory
+#
+# This should be used with the LevelDB fork listed here to use additional test options.
+# For more details on the changes see the blog post listed below.
+# https://github.com/mdcallag/leveldb-1
+# http://smalldatum.blogspot.com/2015/04/comparing-leveldb-and-rocksdb-take-2.html
+
+if [ $# -ne 1 ]; then
+ echo -n "./benchmark.sh [fillseq/overwrite/readrandom/readwhilewriting]"
+ exit 0
+fi
+
+# size constants
+K=1024
+M=$((1024 * K))
+G=$((1024 * M))
+
+if [ -z $DB_DIR ]; then
+ echo "DB_DIR is not defined"
+ exit 0
+fi
+
+output_dir=${OUTPUT_DIR:-/tmp/}
+if [ ! -d $output_dir ]; then
+ mkdir -p $output_dir
+fi
+
+# all multithreaded tests run with sync=1 unless
+# $DB_BENCH_NO_SYNC is defined
+syncval="1"
+if [ ! -z $DB_BENCH_NO_SYNC ]; then
+ echo "Turning sync off for all multithreaded tests"
+ syncval="0";
+fi
+
+num_threads=${NUM_THREADS:-16}
+# Only for *whilewriting, *whilemerging
+writes_per_second=${WRITES_PER_SECOND:-$((10 * K))}
+cache_size=${CACHE_SIZE:-$((1 * G))}
+
+num_keys=${NUM_KEYS:-$((1 * G))}
+key_size=20
+value_size=${VALUE_SIZE:-400}
+block_size=${BLOCK_SIZE:-4096}
+
+const_params="
+ --db=$DB_DIR \
+ \
+ --num=$num_keys \
+ --value_size=$value_size \
+ --cache_size=$cache_size \
+ --compression_ratio=0.5 \
+ \
+ --write_buffer_size=$((2 * M)) \
+ \
+ --histogram=1 \
+ \
+ --bloom_bits=10 \
+ --open_files=$((20 * K))"
+
+params_w="$const_params "
+
+function summarize_result {
+ test_out=$1
+ test_name=$2
+ bench_name=$3
+ nthr=$4
+
+ usecs_op=$( grep ^${bench_name} $test_out | awk '{ printf "%.1f", $3 }' )
+ mb_sec=$( grep ^${bench_name} $test_out | awk '{ printf "%.1f", $5 }' )
+ ops=$( grep "^Count:" $test_out | awk '{ print $2 }' )
+ ops_sec=$( echo "scale=0; (1000000.0 * $nthr) / $usecs_op" | bc )
+ avg=$( grep "^Count:" $test_out | awk '{ printf "%.1f", $4 }' )
+ p50=$( grep "^Min:" $test_out | awk '{ printf "%.1f", $4 }' )
+ echo -e "$ops_sec\t$mb_sec\t$usecs_op\t$avg\t$p50\t$test_name" \
+ >> $output_dir/report.txt
+}
+
+function run_fillseq {
+ # This runs with a vector memtable and the WAL disabled to load faster. It is still crash safe and the
+ # client can discover where to restart a load after a crash. I think this is a good way to load.
+ echo "Loading $num_keys keys sequentially"
+ cmd="./db_bench --benchmarks=fillseq \
+ --use_existing_db=0 \
+ --sync=0 \
+ $params_w \
+ --threads=1 \
+ --seed=$( date +%s ) \
+ 2>&1 | tee -a $output_dir/benchmark_fillseq.v${value_size}.log"
+ echo $cmd | tee $output_dir/benchmark_fillseq.v${value_size}.log
+ eval $cmd
+ summarize_result $output_dir/benchmark_fillseq.v${value_size}.log fillseq.v${value_size} fillseq 1
+}
+
+function run_change {
+ operation=$1
+ echo "Do $num_keys random $operation"
+ out_name="benchmark_${operation}.t${num_threads}.s${syncval}.log"
+ cmd="./db_bench --benchmarks=$operation \
+ --use_existing_db=1 \
+ --sync=$syncval \
+ $params_w \
+ --threads=$num_threads \
+ --seed=$( date +%s ) \
+ 2>&1 | tee -a $output_dir/${out_name}"
+ echo $cmd | tee $output_dir/${out_name}
+ eval $cmd
+ summarize_result $output_dir/${out_name} ${operation}.t${num_threads}.s${syncval} $operation $num_threads
+}
+
+function run_readrandom {
+ echo "Reading $num_keys random keys"
+ out_name="benchmark_readrandom.t${num_threads}.log"
+ cmd="./db_bench --benchmarks=readrandom \
+ --use_existing_db=1 \
+ $params_w \
+ --threads=$num_threads \
+ --seed=$( date +%s ) \
+ 2>&1 | tee -a $output_dir/${out_name}"
+ echo $cmd | tee $output_dir/${out_name}
+ eval $cmd
+ summarize_result $output_dir/${out_name} readrandom.t${num_threads} readrandom $num_threads
+}
+
+function run_readwhile {
+ operation=$1
+ echo "Reading $num_keys random keys while $operation"
+ out_name="benchmark_readwhile${operation}.t${num_threads}.log"
+ cmd="./db_bench --benchmarks=readwhile${operation} \
+ --use_existing_db=1 \
+ --sync=$syncval \
+ $params_w \
+ --threads=$num_threads \
+ --writes_per_second=$writes_per_second \
+ --seed=$( date +%s ) \
+ 2>&1 | tee -a $output_dir/${out_name}"
+ echo $cmd | tee $output_dir/${out_name}
+ eval $cmd
+ summarize_result $output_dir/${out_name} readwhile${operation}.t${num_threads} readwhile${operation} $num_threads
+}
+
+function now() {
+ echo `date +"%s"`
+}
+
+report="$output_dir/report.txt"
+schedule="$output_dir/schedule.txt"
+
+echo "===== Benchmark ====="
+
+# Run!!!
+IFS=',' read -a jobs <<< $1
+# shellcheck disable=SC2068
+for job in ${jobs[@]}; do
+
+ if [ $job != debug ]; then
+ echo "Start $job at `date`" | tee -a $schedule
+ fi
+
+ start=$(now)
+ if [ $job = fillseq ]; then
+ run_fillseq
+ elif [ $job = overwrite ]; then
+ run_change overwrite
+ elif [ $job = readrandom ]; then
+ run_readrandom
+ elif [ $job = readwhilewriting ]; then
+ run_readwhile writing
+ elif [ $job = debug ]; then
+ num_keys=1000; # debug
+ echo "Setting num_keys to $num_keys"
+ else
+ echo "unknown job $job"
+ exit
+ fi
+ end=$(now)
+
+ if [ $job != debug ]; then
+ echo "Complete $job in $((end-start)) seconds" | tee -a $schedule
+ fi
+
+ echo -e "ops/sec\tmb/sec\tusec/op\tavg\tp50\tTest"
+ tail -1 $output_dir/report.txt
+
+done
diff --git a/src/rocksdb/tools/blob_dump.cc b/src/rocksdb/tools/blob_dump.cc
new file mode 100644
index 000000000..1f75eb20d
--- /dev/null
+++ b/src/rocksdb/tools/blob_dump.cc
@@ -0,0 +1,111 @@
+// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+// This source code is licensed under both the GPLv2 (found in the
+// COPYING file in the root directory) and Apache 2.0 License
+// (found in the LICENSE.Apache file in the root directory).
+
+#ifndef ROCKSDB_LITE
+#include <getopt.h>
+
+#include <cstdio>
+#include <string>
+#include <unordered_map>
+
+#include "utilities/blob_db/blob_dump_tool.h"
+
+using ROCKSDB_NAMESPACE::Status;
+using ROCKSDB_NAMESPACE::blob_db::BlobDumpTool;
+
+int main(int argc, char** argv) {
+ using DisplayType = BlobDumpTool::DisplayType;
+ const std::unordered_map<std::string, DisplayType> display_types = {
+ {"none", DisplayType::kNone},
+ {"raw", DisplayType::kRaw},
+ {"hex", DisplayType::kHex},
+ {"detail", DisplayType::kDetail},
+ };
+ const struct option options[] = {
+ {"help", no_argument, nullptr, 'h'},
+ {"file", required_argument, nullptr, 'f'},
+ {"show_key", optional_argument, nullptr, 'k'},
+ {"show_blob", optional_argument, nullptr, 'b'},
+ {"show_uncompressed_blob", optional_argument, nullptr, 'r'},
+ {"show_summary", optional_argument, nullptr, 's'},
+ };
+ DisplayType show_key = DisplayType::kRaw;
+ DisplayType show_blob = DisplayType::kNone;
+ DisplayType show_uncompressed_blob = DisplayType::kNone;
+ bool show_summary = false;
+ std::string file;
+ while (true) {
+ int c = getopt_long(argc, argv, "hk::b::f:", options, nullptr);
+ if (c < 0) {
+ break;
+ }
+ std::string arg_str(optarg ? optarg : "");
+ switch (c) {
+ case 'h':
+ fprintf(stdout,
+ "Usage: blob_dump --file=filename "
+ "[--show_key[=none|raw|hex|detail]] "
+ "[--show_blob[=none|raw|hex|detail]] "
+ "[--show_uncompressed_blob[=none|raw|hex|detail]] "
+ "[--show_summary]\n");
+ return 0;
+ case 'f':
+ file = optarg;
+ break;
+ case 'k':
+ if (optarg) {
+ if (display_types.count(arg_str) == 0) {
+ fprintf(stderr, "Unrecognized key display type.\n");
+ return -1;
+ }
+ show_key = display_types.at(arg_str);
+ }
+ break;
+ case 'b':
+ if (optarg) {
+ if (display_types.count(arg_str) == 0) {
+ fprintf(stderr, "Unrecognized blob display type.\n");
+ return -1;
+ }
+ show_blob = display_types.at(arg_str);
+ } else {
+ show_blob = DisplayType::kHex;
+ }
+ break;
+ case 'r':
+ if (optarg) {
+ if (display_types.count(arg_str) == 0) {
+ fprintf(stderr, "Unrecognized blob display type.\n");
+ return -1;
+ }
+ show_uncompressed_blob = display_types.at(arg_str);
+ } else {
+ show_uncompressed_blob = DisplayType::kHex;
+ }
+ break;
+ case 's':
+ show_summary = true;
+ break;
+ default:
+ fprintf(stderr, "Unrecognized option.\n");
+ return -1;
+ }
+ }
+ BlobDumpTool tool;
+ Status s =
+ tool.Run(file, show_key, show_blob, show_uncompressed_blob, show_summary);
+ if (!s.ok()) {
+ fprintf(stderr, "Failed: %s\n", s.ToString().c_str());
+ return -1;
+ }
+ return 0;
+}
+#else
+#include <stdio.h>
+int main(int /*argc*/, char** /*argv*/) {
+ fprintf(stderr, "Not supported in lite mode.\n");
+ return -1;
+}
+#endif // ROCKSDB_LITE
diff --git a/src/rocksdb/tools/block_cache_analyzer/__init__.py b/src/rocksdb/tools/block_cache_analyzer/__init__.py
new file mode 100644
index 000000000..8dbe96a78
--- /dev/null
+++ b/src/rocksdb/tools/block_cache_analyzer/__init__.py
@@ -0,0 +1,2 @@
+#!/usr/bin/env python3
+# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
diff --git a/src/rocksdb/tools/block_cache_analyzer/block_cache_pysim.py b/src/rocksdb/tools/block_cache_analyzer/block_cache_pysim.py
new file mode 100644
index 000000000..67307df53
--- /dev/null
+++ b/src/rocksdb/tools/block_cache_analyzer/block_cache_pysim.py
@@ -0,0 +1,2000 @@
+#!/usr/bin/env python3
+# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
+
+import gc
+import heapq
+import random
+import sys
+import time
+from collections import OrderedDict
+from os import path
+
+import numpy as np
+
+
+kSampleSize = 64 # The sample size used when performing eviction.
+kMicrosInSecond = 1000000
+kSecondsInMinute = 60
+kSecondsInHour = 3600
+
+
+class TraceRecord:
+ """
+ A trace record represents a block access.
+ It holds the same struct as BlockCacheTraceRecord in
+ trace_replay/block_cache_tracer.h
+ """
+
+ def __init__(
+ self,
+ access_time,
+ block_id,
+ block_type,
+ block_size,
+ cf_id,
+ cf_name,
+ level,
+ fd,
+ caller,
+ no_insert,
+ get_id,
+ key_id,
+ kv_size,
+ is_hit,
+ referenced_key_exist_in_block,
+ num_keys_in_block,
+ table_id,
+ seq_number,
+ block_key_size,
+ key_size,
+ block_offset_in_file,
+ next_access_seq_no,
+ ):
+ self.access_time = access_time
+ self.block_id = block_id
+ self.block_type = block_type
+ self.block_size = block_size + block_key_size
+ self.cf_id = cf_id
+ self.cf_name = cf_name
+ self.level = level
+ self.fd = fd
+ self.caller = caller
+ if no_insert == 1:
+ self.no_insert = True
+ else:
+ self.no_insert = False
+ self.get_id = get_id
+ self.key_id = key_id
+ self.kv_size = kv_size
+ if is_hit == 1:
+ self.is_hit = True
+ else:
+ self.is_hit = False
+ if referenced_key_exist_in_block == 1:
+ self.referenced_key_exist_in_block = True
+ else:
+ self.referenced_key_exist_in_block = False
+ self.num_keys_in_block = num_keys_in_block
+ self.table_id = table_id
+ self.seq_number = seq_number
+ self.block_key_size = block_key_size
+ self.key_size = key_size
+ self.block_offset_in_file = block_offset_in_file
+ self.next_access_seq_no = next_access_seq_no
+
+
+class CacheEntry:
+ """A cache entry stored in the cache."""
+
+ def __init__(
+ self,
+ value_size,
+ cf_id,
+ level,
+ block_type,
+ table_id,
+ access_number,
+ time_s,
+ num_hits=0,
+ ):
+ self.value_size = value_size
+ self.last_access_number = access_number
+ self.num_hits = num_hits
+ self.cf_id = 0
+ self.level = level
+ self.block_type = block_type
+ self.last_access_time = time_s
+ self.insertion_time = time_s
+ self.table_id = table_id
+
+ def __repr__(self):
+ """Debug string."""
+ return "(s={},last={},hits={},cf={},l={},bt={})\n".format(
+ self.value_size,
+ self.last_access_number,
+ self.num_hits,
+ self.cf_id,
+ self.level,
+ self.block_type,
+ )
+
+ def cost_class(self, cost_class_label):
+ if cost_class_label == "table_bt":
+ return "{}-{}".format(self.table_id, self.block_type)
+ elif cost_class_label == "table":
+ return "{}".format(self.table_id)
+ elif cost_class_label == "bt":
+ return "{}".format(self.block_type)
+ elif cost_class_label == "cf":
+ return "{}".format(self.cf_id)
+ elif cost_class_label == "cf_bt":
+ return "{}-{}".format(self.cf_id, self.block_type)
+ elif cost_class_label == "table_level_bt":
+ return "{}-{}-{}".format(self.table_id, self.level, self.block_type)
+ assert False, "Unknown cost class label {}".format(cost_class_label)
+ return None
+
+
+class HashEntry:
+ """A hash entry stored in a hash table."""
+
+ def __init__(self, key, hash, value):
+ self.key = key
+ self.hash = hash
+ self.value = value
+
+ def __repr__(self):
+ return "k={},h={},v=[{}]".format(self.key, self.hash, self.value)
+
+
+class HashTable:
+ """
+ A custom implementation of hash table to support fast random sampling.
+ It is closed hashing and uses chaining to resolve hash conflicts.
+ It grows/shrinks the hash table upon insertion/deletion to support
+ fast lookups and random samplings.
+ """
+
+ def __init__(self):
+ self.initial_size = 32
+ self.table = [None] * self.initial_size
+ self.elements = 0
+
+ def random_sample(self, sample_size):
+ """Randomly sample 'sample_size' hash entries from the table."""
+ samples = []
+ index = random.randint(0, len(self.table) - 1)
+ pos = index
+ # Starting from index, adding hash entries to the sample list until
+ # sample_size is met or we ran out of entries.
+ while True:
+ if self.table[pos] is not None:
+ for i in range(len(self.table[pos])):
+ if self.table[pos][i] is None:
+ continue
+ samples.append(self.table[pos][i])
+ if len(samples) == sample_size:
+ break
+ pos += 1
+ pos = pos % len(self.table)
+ if pos == index or len(samples) == sample_size:
+ break
+ assert len(samples) <= sample_size
+ return samples
+
+ def __repr__(self):
+ all_entries = []
+ for i in range(len(self.table)):
+ if self.table[i] is None:
+ continue
+ for j in range(len(self.table[i])):
+ if self.table[i][j] is not None:
+ all_entries.append(self.table[i][j])
+ return "{}".format(all_entries)
+
+ def values(self):
+ all_values = []
+ for i in range(len(self.table)):
+ if self.table[i] is None:
+ continue
+ for j in range(len(self.table[i])):
+ if self.table[i][j] is not None:
+ all_values.append(self.table[i][j].value)
+ return all_values
+
+ def __len__(self):
+ return self.elements
+
+ def insert(self, key, hash, value):
+ """
+ Insert a hash entry in the table. Replace the old entry if it already
+ exists.
+ """
+ self.grow()
+ inserted = False
+ index = hash % len(self.table)
+ if self.table[index] is None:
+ self.table[index] = []
+ # Search for the entry first.
+ for i in range(len(self.table[index])):
+ if self.table[index][i] is None:
+ continue
+ if self.table[index][i].hash == hash and self.table[index][i].key == key:
+ # The entry already exists in the table.
+ self.table[index][i] = HashEntry(key, hash, value)
+ return
+
+ # Find an empty slot.
+ for i in range(len(self.table[index])):
+ if self.table[index][i] is None:
+ self.table[index][i] = HashEntry(key, hash, value)
+ inserted = True
+ break
+ if not inserted:
+ self.table[index].append(HashEntry(key, hash, value))
+ self.elements += 1
+
+ def resize(self, new_size):
+ if new_size == len(self.table):
+ return
+ if new_size < self.initial_size:
+ return
+ if self.elements < 100:
+ return
+ new_table = [None] * new_size
+ # Copy 'self.table' to new_table.
+ for i in range(len(self.table)):
+ entries = self.table[i]
+ if entries is None:
+ continue
+ for j in range(len(entries)):
+ if entries[j] is None:
+ continue
+ index = entries[j].hash % new_size
+ if new_table[index] is None:
+ new_table[index] = []
+ new_table[index].append(entries[j])
+ self.table = new_table
+ del new_table
+ # Manually call python gc here to free the memory as 'self.table'
+ # might be very large.
+ gc.collect()
+
+ def grow(self):
+ if self.elements < 4 * len(self.table):
+ return
+ new_size = int(len(self.table) * 1.5)
+ self.resize(new_size)
+
+ def delete(self, key, hash):
+ index = hash % len(self.table)
+ deleted = False
+ deleted_entry = None
+ if self.table[index] is None:
+ return
+ for i in range(len(self.table[index])):
+ if (
+ self.table[index][i] is not None
+ and self.table[index][i].hash == hash
+ and self.table[index][i].key == key
+ ):
+ deleted_entry = self.table[index][i]
+ self.table[index][i] = None
+ self.elements -= 1
+ deleted = True
+ break
+ if deleted:
+ self.shrink()
+ return deleted_entry
+
+ def shrink(self):
+ if self.elements * 2 >= len(self.table):
+ return
+ new_size = int(len(self.table) * 0.7)
+ self.resize(new_size)
+
+ def lookup(self, key, hash):
+ index = hash % len(self.table)
+ if self.table[index] is None:
+ return None
+ for i in range(len(self.table[index])):
+ if (
+ self.table[index][i] is not None
+ and self.table[index][i].hash == hash
+ and self.table[index][i].key == key
+ ):
+ return self.table[index][i].value
+ return None
+
+
+class MissRatioStats:
+ def __init__(self, time_unit):
+ self.num_misses = 0
+ self.num_accesses = 0
+ self.time_unit = time_unit
+ self.time_misses = {}
+ self.time_miss_bytes = {}
+ self.time_accesses = {}
+
+ def update_metrics(self, access_time, is_hit, miss_bytes):
+ access_time /= kMicrosInSecond * self.time_unit
+ self.num_accesses += 1
+ if access_time not in self.time_accesses:
+ self.time_accesses[access_time] = 0
+ self.time_accesses[access_time] += 1
+ if not is_hit:
+ self.num_misses += 1
+ if access_time not in self.time_misses:
+ self.time_misses[access_time] = 0
+ self.time_miss_bytes[access_time] = 0
+ self.time_misses[access_time] += 1
+ self.time_miss_bytes[access_time] += miss_bytes
+
+ def reset_counter(self):
+ self.num_misses = 0
+ self.num_accesses = 0
+ self.time_miss_bytes.clear()
+ self.time_misses.clear()
+ self.time_accesses.clear()
+
+ def compute_miss_bytes(self):
+ miss_bytes = []
+ for at in self.time_miss_bytes:
+ miss_bytes.append(self.time_miss_bytes[at])
+ miss_bytes = sorted(miss_bytes)
+ avg_miss_bytes = 0
+ p95_miss_bytes = 0
+ for i in range(len(miss_bytes)):
+ avg_miss_bytes += float(miss_bytes[i]) / float(len(miss_bytes))
+
+ p95_index = min(int(0.95 * float(len(miss_bytes))), len(miss_bytes) - 1)
+ p95_miss_bytes = miss_bytes[p95_index]
+ return avg_miss_bytes, p95_miss_bytes
+
+ def miss_ratio(self):
+ return float(self.num_misses) * 100.0 / float(self.num_accesses)
+
+ def write_miss_timeline(
+ self, cache_type, cache_size, target_cf_name, result_dir, start, end
+ ):
+ start /= kMicrosInSecond * self.time_unit
+ end /= kMicrosInSecond * self.time_unit
+ header_file_path = "{}/header-ml-miss-timeline-{}-{}-{}-{}".format(
+ result_dir, self.time_unit, cache_type, cache_size, target_cf_name
+ )
+ if not path.exists(header_file_path):
+ with open(header_file_path, "w+") as header_file:
+ header = "time"
+ for trace_time in range(start, end):
+ header += ",{}".format(trace_time)
+ header_file.write(header + "\n")
+ file_path = "{}/data-ml-miss-timeline-{}-{}-{}-{}".format(
+ result_dir, self.time_unit, cache_type, cache_size, target_cf_name
+ )
+ with open(file_path, "w+") as file:
+ row = "{}".format(cache_type)
+ for trace_time in range(start, end):
+ row += ",{}".format(self.time_misses.get(trace_time, 0))
+ file.write(row + "\n")
+
+ def write_miss_ratio_timeline(
+ self, cache_type, cache_size, target_cf_name, result_dir, start, end
+ ):
+ start /= kMicrosInSecond * self.time_unit
+ end /= kMicrosInSecond * self.time_unit
+ header_file_path = "{}/header-ml-miss-ratio-timeline-{}-{}-{}-{}".format(
+ result_dir, self.time_unit, cache_type, cache_size, target_cf_name
+ )
+ if not path.exists(header_file_path):
+ with open(header_file_path, "w+") as header_file:
+ header = "time"
+ for trace_time in range(start, end):
+ header += ",{}".format(trace_time)
+ header_file.write(header + "\n")
+ file_path = "{}/data-ml-miss-ratio-timeline-{}-{}-{}-{}".format(
+ result_dir, self.time_unit, cache_type, cache_size, target_cf_name
+ )
+ with open(file_path, "w+") as file:
+ row = "{}".format(cache_type)
+ for trace_time in range(start, end):
+ naccesses = self.time_accesses.get(trace_time, 0)
+ miss_ratio = 0
+ if naccesses > 0:
+ miss_ratio = float(
+ self.time_misses.get(trace_time, 0) * 100.0
+ ) / float(naccesses)
+ row += ",{0:.2f}".format(miss_ratio)
+ file.write(row + "\n")
+
+
+class PolicyStats:
+ def __init__(self, time_unit, policies):
+ self.time_selected_polices = {}
+ self.time_accesses = {}
+ self.policy_names = {}
+ self.time_unit = time_unit
+ for i in range(len(policies)):
+ self.policy_names[i] = policies[i].policy_name()
+
+ def update_metrics(self, access_time, selected_policy):
+ access_time /= kMicrosInSecond * self.time_unit
+ if access_time not in self.time_accesses:
+ self.time_accesses[access_time] = 0
+ self.time_accesses[access_time] += 1
+ if access_time not in self.time_selected_polices:
+ self.time_selected_polices[access_time] = {}
+ policy_name = self.policy_names[selected_policy]
+ if policy_name not in self.time_selected_polices[access_time]:
+ self.time_selected_polices[access_time][policy_name] = 0
+ self.time_selected_polices[access_time][policy_name] += 1
+
+ def write_policy_timeline(
+ self, cache_type, cache_size, target_cf_name, result_dir, start, end
+ ):
+ start /= kMicrosInSecond * self.time_unit
+ end /= kMicrosInSecond * self.time_unit
+ header_file_path = "{}/header-ml-policy-timeline-{}-{}-{}-{}".format(
+ result_dir, self.time_unit, cache_type, cache_size, target_cf_name
+ )
+ if not path.exists(header_file_path):
+ with open(header_file_path, "w+") as header_file:
+ header = "time"
+ for trace_time in range(start, end):
+ header += ",{}".format(trace_time)
+ header_file.write(header + "\n")
+ file_path = "{}/data-ml-policy-timeline-{}-{}-{}-{}".format(
+ result_dir, self.time_unit, cache_type, cache_size, target_cf_name
+ )
+ with open(file_path, "w+") as file:
+ for policy in self.policy_names:
+ policy_name = self.policy_names[policy]
+ row = "{}-{}".format(cache_type, policy_name)
+ for trace_time in range(start, end):
+ row += ",{}".format(
+ self.time_selected_polices.get(trace_time, {}).get(
+ policy_name, 0
+ )
+ )
+ file.write(row + "\n")
+
+ def write_policy_ratio_timeline(
+ self, cache_type, cache_size, target_cf_name, file_path, start, end
+ ):
+ start /= kMicrosInSecond * self.time_unit
+ end /= kMicrosInSecond * self.time_unit
+ header_file_path = "{}/header-ml-policy-ratio-timeline-{}-{}-{}-{}".format(
+ result_dir, self.time_unit, cache_type, cache_size, target_cf_name
+ )
+ if not path.exists(header_file_path):
+ with open(header_file_path, "w+") as header_file:
+ header = "time"
+ for trace_time in range(start, end):
+ header += ",{}".format(trace_time)
+ header_file.write(header + "\n")
+ file_path = "{}/data-ml-policy-ratio-timeline-{}-{}-{}-{}".format(
+ result_dir, self.time_unit, cache_type, cache_size, target_cf_name
+ )
+ with open(file_path, "w+") as file:
+ for policy in self.policy_names:
+ policy_name = self.policy_names[policy]
+ row = "{}-{}".format(cache_type, policy_name)
+ for trace_time in range(start, end):
+ naccesses = self.time_accesses.get(trace_time, 0)
+ ratio = 0
+ if naccesses > 0:
+ ratio = float(
+ self.time_selected_polices.get(trace_time, {}).get(
+ policy_name, 0
+ )
+ * 100.0
+ ) / float(naccesses)
+ row += ",{0:.2f}".format(ratio)
+ file.write(row + "\n")
+
+
+class Policy(object):
+ """
+ A policy maintains a set of evicted keys. It returns a reward of one to
+ itself if it has not evicted a missing key. Otherwise, it gives itself 0
+ reward.
+ """
+
+ def __init__(self):
+ self.evicted_keys = {}
+
+ def evict(self, key, max_size):
+ self.evicted_keys[key] = 0
+
+ def delete(self, key):
+ self.evicted_keys.pop(key, None)
+
+ def prioritize_samples(self, samples, auxilliary_info):
+ raise NotImplementedError
+
+ def policy_name(self):
+ raise NotImplementedError
+
+ def generate_reward(self, key):
+ if key in self.evicted_keys:
+ return 0
+ return 1
+
+
+class LRUPolicy(Policy):
+ def prioritize_samples(self, samples, auxilliary_info):
+ return sorted(
+ samples,
+ cmp=lambda e1, e2: e1.value.last_access_number
+ - e2.value.last_access_number,
+ )
+
+ def policy_name(self):
+ return "lru"
+
+
+class MRUPolicy(Policy):
+ def prioritize_samples(self, samples, auxilliary_info):
+ return sorted(
+ samples,
+ cmp=lambda e1, e2: e2.value.last_access_number
+ - e1.value.last_access_number,
+ )
+
+ def policy_name(self):
+ return "mru"
+
+
+class LFUPolicy(Policy):
+ def prioritize_samples(self, samples, auxilliary_info):
+ return sorted(samples, cmp=lambda e1, e2: e1.value.num_hits - e2.value.num_hits)
+
+ def policy_name(self):
+ return "lfu"
+
+
+class HyperbolicPolicy(Policy):
+ """
+ An implementation of Hyperbolic caching.
+
+ Aaron Blankstein, Siddhartha Sen, and Michael J. Freedman. 2017.
+ Hyperbolic caching: flexible caching for web applications. In Proceedings
+ of the 2017 USENIX Conference on Usenix Annual Technical Conference
+ (USENIX ATC '17). USENIX Association, Berkeley, CA, USA, 499-511.
+ """
+
+ def compare(self, e1, e2, now):
+ e1_duration = max(0, (now - e1.value.insertion_time) / kMicrosInSecond) * float(
+ e1.value.value_size
+ )
+ e2_duration = max(0, (now - e2.value.insertion_time) / kMicrosInSecond) * float(
+ e2.value.value_size
+ )
+ if e1_duration == e2_duration:
+ return e1.value.num_hits - e2.value.num_hits
+ if e1_duration == 0:
+ return 1
+ if e2_duration == 0:
+ return 1
+ diff = (float(e1.value.num_hits) / (float(e1_duration))) - (
+ float(e2.value.num_hits) / float(e2_duration)
+ )
+ if diff == 0:
+ return 0
+ elif diff > 0:
+ return 1
+ else:
+ return -1
+
+ def prioritize_samples(self, samples, auxilliary_info):
+ assert len(auxilliary_info) == 3
+ now = auxilliary_info[0]
+ return sorted(samples, cmp=lambda e1, e2: self.compare(e1, e2, now))
+
+ def policy_name(self):
+ return "hb"
+
+
+class CostClassPolicy(Policy):
+ """
+ We calculate the hit density of a cost class as
+ number of hits / total size in cache * average duration in the cache.
+
+ An entry has a higher priority if its class's hit density is higher.
+ """
+
+ def compare(self, e1, e2, now, cost_classes, cost_class_label):
+ e1_class = e1.value.cost_class(cost_class_label)
+ e2_class = e2.value.cost_class(cost_class_label)
+
+ assert e1_class in cost_classes
+ assert e2_class in cost_classes
+
+ e1_entry = cost_classes[e1_class]
+ e2_entry = cost_classes[e2_class]
+ e1_density = e1_entry.density(now)
+ e2_density = e2_entry.density(now)
+ e1_hits = cost_classes[e1_class].hits
+ e2_hits = cost_classes[e2_class].hits
+
+ if e1_density == e2_density:
+ return e1_hits - e2_hits
+
+ if e1_entry.num_entries_in_cache == 0:
+ return -1
+ if e2_entry.num_entries_in_cache == 0:
+ return 1
+
+ if e1_density == 0:
+ return 1
+ if e2_density == 0:
+ return -1
+ diff = (float(e1_hits) / float(e1_density)) - (
+ float(e2_hits) / float(e2_density)
+ )
+ if diff == 0:
+ return 0
+ elif diff > 0:
+ return 1
+ else:
+ return -1
+
+ def prioritize_samples(self, samples, auxilliary_info):
+ assert len(auxilliary_info) == 3
+ now = auxilliary_info[0]
+ cost_classes = auxilliary_info[1]
+ cost_class_label = auxilliary_info[2]
+ return sorted(
+ samples,
+ cmp=lambda e1, e2: self.compare(
+ e1, e2, now, cost_classes, cost_class_label
+ ),
+ )
+
+ def policy_name(self):
+ return "cc"
+
+
+class Cache(object):
+ """
+ This is the base class for the implementations of alternative cache
+ replacement policies.
+ """
+
+ def __init__(self, cache_size, enable_cache_row_key):
+ self.cache_size = cache_size
+ self.used_size = 0
+ self.per_second_miss_ratio_stats = MissRatioStats(1)
+ self.miss_ratio_stats = MissRatioStats(kSecondsInMinute)
+ self.per_hour_miss_ratio_stats = MissRatioStats(kSecondsInHour)
+ # 0: disabled. 1: enabled. Insert both row and the refereneced data block.
+ # 2: enabled. Insert only the row but NOT the referenced data block.
+ self.enable_cache_row_key = enable_cache_row_key
+ self.get_id_row_key_map = {}
+ self.max_seen_get_id = 0
+ self.retain_get_id_range = 100000
+
+ def block_key(self, trace_record):
+ return "b{}".format(trace_record.block_id)
+
+ def row_key(self, trace_record):
+ return "g{}-{}".format(trace_record.fd, trace_record.key_id)
+
+ def _lookup(self, trace_record, key, hash):
+ """
+ Look up the key in the cache.
+ Returns true upon a cache hit, false otherwise.
+ """
+ raise NotImplementedError
+
+ def _evict(self, trace_record, key, hash, value_size):
+ """
+ Evict entries in the cache until there is enough room to insert the new
+ entry with 'value_size'.
+ """
+ raise NotImplementedError
+
+ def _insert(self, trace_record, key, hash, value_size):
+ """
+ Insert the new entry into the cache.
+ """
+ raise NotImplementedError
+
+ def _should_admit(self, trace_record, key, hash, value_size):
+ """
+ A custom admission policy to decide whether we should admit the new
+ entry upon a cache miss.
+ Returns true if the new entry should be admitted, false otherwise.
+ """
+ raise NotImplementedError
+
+ def cache_name(self):
+ """
+ The name of the replacement policy.
+ """
+ raise NotImplementedError
+
+ def is_ml_cache(self):
+ return False
+
+ def _update_stats(self, access_time, is_hit, miss_bytes):
+ self.per_second_miss_ratio_stats.update_metrics(access_time, is_hit, miss_bytes)
+ self.miss_ratio_stats.update_metrics(access_time, is_hit, miss_bytes)
+ self.per_hour_miss_ratio_stats.update_metrics(access_time, is_hit, miss_bytes)
+
+ def access(self, trace_record):
+ """
+ Access a trace record. The simulator calls this function to access a
+ trace record.
+ """
+ assert self.used_size <= self.cache_size
+ if (
+ self.enable_cache_row_key > 0
+ and trace_record.caller == 1
+ and trace_record.key_id != 0
+ and trace_record.get_id != 0
+ ):
+ # This is a get request.
+ self._access_row(trace_record)
+ return
+ is_hit = self._access_kv(
+ trace_record,
+ self.block_key(trace_record),
+ trace_record.block_id,
+ trace_record.block_size,
+ trace_record.no_insert,
+ )
+ self._update_stats(
+ trace_record.access_time, is_hit=is_hit, miss_bytes=trace_record.block_size
+ )
+
+ def _access_row(self, trace_record):
+ row_key = self.row_key(trace_record)
+ self.max_seen_get_id = max(self.max_seen_get_id, trace_record.get_id)
+ self.get_id_row_key_map.pop(
+ self.max_seen_get_id - self.retain_get_id_range, None
+ )
+ if trace_record.get_id not in self.get_id_row_key_map:
+ self.get_id_row_key_map[trace_record.get_id] = {}
+ self.get_id_row_key_map[trace_record.get_id]["h"] = False
+ if self.get_id_row_key_map[trace_record.get_id]["h"]:
+ # We treat future accesses as hits since this get request
+ # completes.
+ # print("row hit 1")
+ self._update_stats(trace_record.access_time, is_hit=True, miss_bytes=0)
+ return
+ if row_key not in self.get_id_row_key_map[trace_record.get_id]:
+ # First time seen this key.
+ is_hit = self._access_kv(
+ trace_record,
+ key=row_key,
+ hash=trace_record.key_id,
+ value_size=trace_record.kv_size,
+ no_insert=False,
+ )
+ inserted = False
+ if trace_record.kv_size > 0:
+ inserted = True
+ self.get_id_row_key_map[trace_record.get_id][row_key] = inserted
+ self.get_id_row_key_map[trace_record.get_id]["h"] = is_hit
+ if self.get_id_row_key_map[trace_record.get_id]["h"]:
+ # We treat future accesses as hits since this get request
+ # completes.
+ # print("row hit 2")
+ self._update_stats(trace_record.access_time, is_hit=True, miss_bytes=0)
+ return
+ # Access its blocks.
+ no_insert = trace_record.no_insert
+ if (
+ self.enable_cache_row_key == 2
+ and trace_record.kv_size > 0
+ and trace_record.block_type == 9
+ ):
+ no_insert = True
+ is_hit = self._access_kv(
+ trace_record,
+ key=self.block_key(trace_record),
+ hash=trace_record.block_id,
+ value_size=trace_record.block_size,
+ no_insert=no_insert,
+ )
+ self._update_stats(
+ trace_record.access_time, is_hit, miss_bytes=trace_record.block_size
+ )
+ if (
+ trace_record.kv_size > 0
+ and not self.get_id_row_key_map[trace_record.get_id][row_key]
+ ):
+ # Insert the row key-value pair.
+ self._access_kv(
+ trace_record,
+ key=row_key,
+ hash=trace_record.key_id,
+ value_size=trace_record.kv_size,
+ no_insert=False,
+ )
+ # Mark as inserted.
+ self.get_id_row_key_map[trace_record.get_id][row_key] = True
+
+ def _access_kv(self, trace_record, key, hash, value_size, no_insert):
+ # Sanity checks.
+ assert self.used_size <= self.cache_size
+ if self._lookup(trace_record, key, hash):
+ # A cache hit.
+ return True
+ if no_insert or value_size <= 0:
+ return False
+ # A cache miss.
+ if value_size > self.cache_size:
+ # The block is too large to fit into the cache.
+ return False
+ self._evict(trace_record, key, hash, value_size)
+ if self._should_admit(trace_record, key, hash, value_size):
+ self._insert(trace_record, key, hash, value_size)
+ self.used_size += value_size
+ return False
+
+
+class CostClassEntry:
+ """
+ A cost class maintains aggregated statistics of cached entries in a class.
+ For example, we may define block type as a class. Then, cached blocks of the
+ same type will share one cost class entry.
+ """
+
+ def __init__(self):
+ self.hits = 0
+ self.num_entries_in_cache = 0
+ self.size_in_cache = 0
+ self.sum_insertion_times = 0
+ self.sum_last_access_time = 0
+
+ def insert(self, trace_record, key, value_size):
+ self.size_in_cache += value_size
+ self.num_entries_in_cache += 1
+ self.sum_insertion_times += trace_record.access_time / kMicrosInSecond
+ self.sum_last_access_time += trace_record.access_time / kMicrosInSecond
+
+ def remove(self, insertion_time, last_access_time, key, value_size, num_hits):
+ self.hits -= num_hits
+ self.num_entries_in_cache -= 1
+ self.sum_insertion_times -= insertion_time / kMicrosInSecond
+ self.size_in_cache -= value_size
+ self.sum_last_access_time -= last_access_time / kMicrosInSecond
+
+ def update_on_hit(self, trace_record, last_access_time):
+ self.hits += 1
+ self.sum_last_access_time -= last_access_time / kMicrosInSecond
+ self.sum_last_access_time += trace_record.access_time / kMicrosInSecond
+
+ def avg_lifetime_in_cache(self, now):
+ avg_insertion_time = self.sum_insertion_times / self.num_entries_in_cache
+ return now / kMicrosInSecond - avg_insertion_time
+
+ def avg_last_access_time(self):
+ if self.num_entries_in_cache == 0:
+ return 0
+ return float(self.sum_last_access_time) / float(self.num_entries_in_cache)
+
+ def avg_size(self):
+ if self.num_entries_in_cache == 0:
+ return 0
+ return float(self.sum_last_access_time) / float(self.num_entries_in_cache)
+
+ def density(self, now):
+ avg_insertion_time = self.sum_insertion_times / self.num_entries_in_cache
+ in_cache_duration = now / kMicrosInSecond - avg_insertion_time
+ return self.size_in_cache * in_cache_duration
+
+
+class MLCache(Cache):
+ """
+ MLCache is the base class for implementations of alternative replacement
+ policies using reinforcement learning.
+ """
+
+ def __init__(self, cache_size, enable_cache_row_key, policies, cost_class_label):
+ super(MLCache, self).__init__(cache_size, enable_cache_row_key)
+ self.table = HashTable()
+ self.policy_stats = PolicyStats(kSecondsInMinute, policies)
+ self.per_hour_policy_stats = PolicyStats(kSecondsInHour, policies)
+ self.policies = policies
+ self.cost_classes = {}
+ self.cost_class_label = cost_class_label
+
+ def is_ml_cache(self):
+ return True
+
+ def _lookup(self, trace_record, key, hash):
+ value = self.table.lookup(key, hash)
+ if value is not None:
+ # Update the entry's cost class statistics.
+ if self.cost_class_label is not None:
+ cost_class = value.cost_class(self.cost_class_label)
+ assert cost_class in self.cost_classes
+ self.cost_classes[cost_class].update_on_hit(
+ trace_record, value.last_access_time
+ )
+ # Update the entry's last access time.
+ self.table.insert(
+ key,
+ hash,
+ CacheEntry(
+ value_size=value.value_size,
+ cf_id=value.cf_id,
+ level=value.level,
+ block_type=value.block_type,
+ table_id=value.table_id,
+ access_number=self.miss_ratio_stats.num_accesses,
+ time_s=trace_record.access_time,
+ num_hits=value.num_hits + 1,
+ ),
+ )
+ return True
+ return False
+
+ def _evict(self, trace_record, key, hash, value_size):
+ # Select a policy, random sample kSampleSize keys from the cache, then
+ # evict keys in the sample set until we have enough room for the new
+ # entry.
+ policy_index = self._select_policy(trace_record, key)
+ assert policy_index < len(self.policies) and policy_index >= 0
+ self.policies[policy_index].delete(key)
+ self.policy_stats.update_metrics(trace_record.access_time, policy_index)
+ self.per_hour_policy_stats.update_metrics(
+ trace_record.access_time, policy_index
+ )
+ while self.used_size + value_size > self.cache_size:
+ # Randomly sample n entries.
+ samples = self.table.random_sample(kSampleSize)
+ samples = self.policies[policy_index].prioritize_samples(
+ samples,
+ [trace_record.access_time, self.cost_classes, self.cost_class_label],
+ )
+ for hash_entry in samples:
+ assert self.table.delete(hash_entry.key, hash_entry.hash) is not None
+ self.used_size -= hash_entry.value.value_size
+ self.policies[policy_index].evict(
+ key=hash_entry.key, max_size=self.table.elements
+ )
+ # Update the entry's cost class statistics.
+ if self.cost_class_label is not None:
+ cost_class = hash_entry.value.cost_class(self.cost_class_label)
+ assert cost_class in self.cost_classes
+ self.cost_classes[cost_class].remove(
+ hash_entry.value.insertion_time,
+ hash_entry.value.last_access_time,
+ key,
+ hash_entry.value.value_size,
+ hash_entry.value.num_hits,
+ )
+ if self.used_size + value_size <= self.cache_size:
+ break
+
+ def _insert(self, trace_record, key, hash, value_size):
+ assert self.used_size + value_size <= self.cache_size
+ entry = CacheEntry(
+ value_size,
+ trace_record.cf_id,
+ trace_record.level,
+ trace_record.block_type,
+ trace_record.table_id,
+ self.miss_ratio_stats.num_accesses,
+ trace_record.access_time,
+ )
+ # Update the entry's cost class statistics.
+ if self.cost_class_label is not None:
+ cost_class = entry.cost_class(self.cost_class_label)
+ if cost_class not in self.cost_classes:
+ self.cost_classes[cost_class] = CostClassEntry()
+ self.cost_classes[cost_class].insert(trace_record, key, value_size)
+ self.table.insert(key, hash, entry)
+
+ def _should_admit(self, trace_record, key, hash, value_size):
+ return True
+
+ def _select_policy(self, trace_record, key):
+ raise NotImplementedError
+
+
+class ThompsonSamplingCache(MLCache):
+ """
+ An implementation of Thompson Sampling for the Bernoulli Bandit.
+
+ Daniel J. Russo, Benjamin Van Roy, Abbas Kazerouni, Ian Osband,
+ and Zheng Wen. 2018. A Tutorial on Thompson Sampling. Found.
+ Trends Mach. Learn. 11, 1 (July 2018), 1-96.
+ DOI: https://doi.org/10.1561/2200000070
+ """
+
+ def __init__(
+ self,
+ cache_size,
+ enable_cache_row_key,
+ policies,
+ cost_class_label,
+ init_a=1,
+ init_b=1,
+ ):
+ super(ThompsonSamplingCache, self).__init__(
+ cache_size, enable_cache_row_key, policies, cost_class_label
+ )
+ self._as = {}
+ self._bs = {}
+ for _i in range(len(policies)):
+ self._as = [init_a] * len(self.policies)
+ self._bs = [init_b] * len(self.policies)
+
+ def _select_policy(self, trace_record, key):
+ if len(self.policies) == 1:
+ return 0
+ samples = [
+ np.random.beta(self._as[x], self._bs[x]) for x in range(len(self.policies))
+ ]
+ selected_policy = max(range(len(self.policies)), key=lambda x: samples[x])
+ reward = self.policies[selected_policy].generate_reward(key)
+ assert reward <= 1 and reward >= 0
+ self._as[selected_policy] += reward
+ self._bs[selected_policy] += 1 - reward
+ return selected_policy
+
+ def cache_name(self):
+ if self.enable_cache_row_key:
+ return "Hybrid ThompsonSampling with cost class {} (ts_hybrid)".format(
+ self.cost_class_label
+ )
+ return "ThompsonSampling with cost class {} (ts)".format(self.cost_class_label)
+
+
+class LinUCBCache(MLCache):
+ """
+ An implementation of LinUCB with disjoint linear models.
+
+ Lihong Li, Wei Chu, John Langford, and Robert E. Schapire. 2010.
+ A contextual-bandit approach to personalized news article recommendation.
+ In Proceedings of the 19th international conference on World wide web
+ (WWW '10). ACM, New York, NY, USA, 661-670.
+ DOI=http://dx.doi.org/10.1145/1772690.1772758
+ """
+
+ def __init__(self, cache_size, enable_cache_row_key, policies, cost_class_label):
+ super(LinUCBCache, self).__init__(
+ cache_size, enable_cache_row_key, policies, cost_class_label
+ )
+ self.nfeatures = 4 # Block type, level, cf.
+ self.th = np.zeros((len(self.policies), self.nfeatures))
+ self.eps = 0.2
+ self.b = np.zeros_like(self.th)
+ self.A = np.zeros((len(self.policies), self.nfeatures, self.nfeatures))
+ self.A_inv = np.zeros((len(self.policies), self.nfeatures, self.nfeatures))
+ for i in range(len(self.policies)):
+ self.A[i] = np.identity(self.nfeatures)
+ self.th_hat = np.zeros_like(self.th)
+ self.p = np.zeros(len(self.policies))
+ self.alph = 0.2
+
+ def _select_policy(self, trace_record, key):
+ if len(self.policies) == 1:
+ return 0
+ x_i = np.zeros(self.nfeatures) # The current context vector
+ x_i[0] = trace_record.block_type
+ x_i[1] = trace_record.level
+ x_i[2] = trace_record.cf_id
+ p = np.zeros(len(self.policies))
+ for a in range(len(self.policies)):
+ self.th_hat[a] = self.A_inv[a].dot(self.b[a])
+ ta = x_i.dot(self.A_inv[a]).dot(x_i)
+ a_upper_ci = self.alph * np.sqrt(ta)
+ a_mean = self.th_hat[a].dot(x_i)
+ p[a] = a_mean + a_upper_ci
+ p = p + (np.random.random(len(p)) * 0.000001)
+ selected_policy = p.argmax()
+ reward = self.policies[selected_policy].generate_reward(key)
+ assert reward <= 1 and reward >= 0
+ self.A[selected_policy] += np.outer(x_i, x_i)
+ self.b[selected_policy] += reward * x_i
+ self.A_inv[selected_policy] = np.linalg.inv(self.A[selected_policy])
+ del x_i
+ return selected_policy
+
+ def cache_name(self):
+ if self.enable_cache_row_key:
+ return "Hybrid LinUCB with cost class {} (linucb_hybrid)".format(
+ self.cost_class_label
+ )
+ return "LinUCB with cost class {} (linucb)".format(self.cost_class_label)
+
+
+class OPTCacheEntry:
+ """
+ A cache entry for the OPT algorithm. The entries are sorted based on its
+ next access sequence number in reverse order, i.e., the entry which next
+ access is the furthest in the future is ordered before other entries.
+ """
+
+ def __init__(self, key, next_access_seq_no, value_size):
+ self.key = key
+ self.next_access_seq_no = next_access_seq_no
+ self.value_size = value_size
+ self.is_removed = False
+
+ def __cmp__(self, other):
+ if other.next_access_seq_no != self.next_access_seq_no:
+ return other.next_access_seq_no - self.next_access_seq_no
+ return self.value_size - other.value_size
+
+ def __repr__(self):
+ return "({} {} {} {})".format(
+ self.key, self.next_access_seq_no, self.value_size, self.is_removed
+ )
+
+
+class PQTable:
+ """
+ A hash table with a priority queue.
+ """
+
+ def __init__(self):
+ # A list of entries arranged in a heap sorted based on the entry custom
+ # implementation of __cmp__
+ self.pq = []
+ self.table = {}
+
+ def pqinsert(self, entry):
+ "Add a new key or update the priority of an existing key"
+ # Remove the entry from the table first.
+ removed_entry = self.table.pop(entry.key, None)
+ if removed_entry:
+ # Mark as removed since there is no 'remove' API in heappq.
+ # Instead, an entry in pq is removed lazily when calling pop.
+ removed_entry.is_removed = True
+ self.table[entry.key] = entry
+ heapq.heappush(self.pq, entry)
+ return removed_entry
+
+ def pqpop(self):
+ while self.pq:
+ entry = heapq.heappop(self.pq)
+ if not entry.is_removed:
+ del self.table[entry.key]
+ return entry
+ return None
+
+ def pqpeek(self):
+ while self.pq:
+ entry = self.pq[0]
+ if not entry.is_removed:
+ return entry
+ heapq.heappop(self.pq)
+ return
+
+ def __contains__(self, k):
+ return k in self.table
+
+ def __getitem__(self, k):
+ return self.table[k]
+
+ def __len__(self):
+ return len(self.table)
+
+ def values(self):
+ return self.table.values()
+
+
+class OPTCache(Cache):
+ """
+ An implementation of the Belady MIN algorithm. OPTCache evicts an entry
+ in the cache whose next access occurs furthest in the future.
+
+ Note that Belady MIN algorithm is optimal assuming all blocks having the
+ same size and a missing entry will be inserted in the cache.
+ These are NOT true for the block cache trace since blocks have different
+ sizes and we may not insert a block into the cache upon a cache miss.
+ However, it is still useful to serve as a "theoretical upper bound" on the
+ lowest miss ratio we can achieve given a cache size.
+
+ L. A. Belady. 1966. A Study of Replacement Algorithms for a
+ Virtual-storage Computer. IBM Syst. J. 5, 2 (June 1966), 78-101.
+ DOI=http://dx.doi.org/10.1147/sj.52.0078
+ """
+
+ def __init__(self, cache_size):
+ super(OPTCache, self).__init__(cache_size, enable_cache_row_key=0)
+ self.table = PQTable()
+
+ def _lookup(self, trace_record, key, hash):
+ if key not in self.table:
+ return False
+ # A cache hit. Update its next access time.
+ assert (
+ self.table.pqinsert(
+ OPTCacheEntry(
+ key, trace_record.next_access_seq_no, self.table[key].value_size
+ )
+ )
+ is not None
+ )
+ return True
+
+ def _evict(self, trace_record, key, hash, value_size):
+ while self.used_size + value_size > self.cache_size:
+ evict_entry = self.table.pqpop()
+ assert evict_entry is not None
+ self.used_size -= evict_entry.value_size
+
+ def _insert(self, trace_record, key, hash, value_size):
+ assert (
+ self.table.pqinsert(
+ OPTCacheEntry(key, trace_record.next_access_seq_no, value_size)
+ )
+ is None
+ )
+
+ def _should_admit(self, trace_record, key, hash, value_size):
+ return True
+
+ def cache_name(self):
+ return "Belady MIN (opt)"
+
+
+class GDSizeEntry:
+ """
+ A cache entry for the greedy dual size replacement policy.
+ """
+
+ def __init__(self, key, value_size, priority):
+ self.key = key
+ self.value_size = value_size
+ self.priority = priority
+ self.is_removed = False
+
+ def __cmp__(self, other):
+ if other.priority != self.priority:
+ return self.priority - other.priority
+ return self.value_size - other.value_size
+
+ def __repr__(self):
+ return "({} {} {} {})".format(
+ self.key, self.next_access_seq_no, self.value_size, self.is_removed
+ )
+
+
+class GDSizeCache(Cache):
+ """
+ An implementation of the greedy dual size algorithm.
+ We define cost as an entry's size.
+
+ See https://www.usenix.org/legacy/publications/library/proceedings/usits97/full_papers/cao/cao_html/node8.html
+ and N. Young. The k-server dual and loose competitiveness for paging.
+ Algorithmica,June 1994, vol. 11,(no.6):525-41.
+ Rewritten version of ''On-line caching as cache size varies'',
+ in The 2nd Annual ACM-SIAM Symposium on Discrete Algorithms, 241-250, 1991.
+ """
+
+ def __init__(self, cache_size, enable_cache_row_key):
+ super(GDSizeCache, self).__init__(cache_size, enable_cache_row_key)
+ self.table = PQTable()
+ self.L = 0.0
+
+ def cache_name(self):
+ if self.enable_cache_row_key:
+ return "Hybrid GreedyDualSize (gdsize_hybrid)"
+ return "GreedyDualSize (gdsize)"
+
+ def _lookup(self, trace_record, key, hash):
+ if key not in self.table:
+ return False
+ # A cache hit. Update its priority.
+ entry = self.table[key]
+ assert (
+ self.table.pqinsert(
+ GDSizeEntry(key, entry.value_size, self.L + entry.value_size)
+ )
+ is not None
+ )
+ return True
+
+ def _evict(self, trace_record, key, hash, value_size):
+ while self.used_size + value_size > self.cache_size:
+ evict_entry = self.table.pqpop()
+ assert evict_entry is not None
+ self.L = evict_entry.priority
+ self.used_size -= evict_entry.value_size
+
+ def _insert(self, trace_record, key, hash, value_size):
+ assert (
+ self.table.pqinsert(GDSizeEntry(key, value_size, self.L + value_size))
+ is None
+ )
+
+ def _should_admit(self, trace_record, key, hash, value_size):
+ return True
+
+
+class Deque(object):
+ """A Deque class facilitates the implementation of LRU and ARC."""
+
+ def __init__(self):
+ self.od = OrderedDict()
+
+ def appendleft(self, k):
+ if k in self.od:
+ del self.od[k]
+ self.od[k] = None
+
+ def pop(self):
+ item = self.od.popitem(last=False) if self.od else None
+ if item is not None:
+ return item[0]
+ return None
+
+ def remove(self, k):
+ del self.od[k]
+
+ def __len__(self):
+ return len(self.od)
+
+ def __contains__(self, k):
+ return k in self.od
+
+ def __iter__(self):
+ return reversed(self.od)
+
+ def __repr__(self):
+ return "Deque(%r)" % (list(self),)
+
+
+class ARCCache(Cache):
+ """
+ An implementation of ARC. ARC assumes that all blocks are having the
+ same size. The size of index and filter blocks are variable. To accommodate
+ this, we modified ARC as follows:
+ 1) We use 16 KB as the average block size and calculate the number of blocks
+ (c) in the cache.
+ 2) When we insert an entry, the cache evicts entries in both t1 and t2
+ queues until it has enough space for the new entry. This also requires
+ modification of the algorithm to maintain a maximum of 2*c blocks.
+
+ Nimrod Megiddo and Dharmendra S. Modha. 2003. ARC: A Self-Tuning, Low
+ Overhead Replacement Cache. In Proceedings of the 2nd USENIX Conference on
+ File and Storage Technologies (FAST '03). USENIX Association, Berkeley, CA,
+ USA, 115-130.
+ """
+
+ def __init__(self, cache_size, enable_cache_row_key):
+ super(ARCCache, self).__init__(cache_size, enable_cache_row_key)
+ self.table = {}
+ self.c = cache_size / 16 * 1024 # Number of elements in the cache.
+ self.p = 0 # Target size for the list T1
+ # L1: only once recently
+ self.t1 = Deque() # T1: recent cache entries
+ self.b1 = Deque() # B1: ghost entries recently evicted from the T1 cache
+ # L2: at least twice recently
+ self.t2 = Deque() # T2: frequent entries
+ self.b2 = Deque() # B2: ghost entries recently evicted from the T2 cache
+
+ def _replace(self, key, value_size):
+ while self.used_size + value_size > self.cache_size:
+ if self.t1 and ((key in self.b2) or (len(self.t1) > self.p)):
+ old = self.t1.pop()
+ self.b1.appendleft(old)
+ else:
+ if self.t2:
+ old = self.t2.pop()
+ self.b2.appendleft(old)
+ else:
+ old = self.t1.pop()
+ self.b1.appendleft(old)
+ self.used_size -= self.table[old].value_size
+ del self.table[old]
+
+ def _lookup(self, trace_record, key, hash):
+ # Case I: key is in T1 or T2.
+ # Move key to MRU position in T2.
+ if key in self.t1:
+ self.t1.remove(key)
+ self.t2.appendleft(key)
+ return True
+
+ if key in self.t2:
+ self.t2.remove(key)
+ self.t2.appendleft(key)
+ return True
+ return False
+
+ def _evict(self, trace_record, key, hash, value_size):
+ # Case II: key is in B1
+ # Move x from B1 to the MRU position in T2 (also fetch x to the cache).
+ if key in self.b1:
+ self.p = min(self.c, self.p + max(len(self.b2) / len(self.b1), 1))
+ self._replace(key, value_size)
+ self.b1.remove(key)
+ self.t2.appendleft(key)
+ return
+
+ # Case III: key is in B2
+ # Move x from B2 to the MRU position in T2 (also fetch x to the cache).
+ if key in self.b2:
+ self.p = max(0, self.p - max(len(self.b1) / len(self.b2), 1))
+ self._replace(key, value_size)
+ self.b2.remove(key)
+ self.t2.appendleft(key)
+ return
+
+ # Case IV: key is not in (T1 u B1 u T2 u B2)
+ self._replace(key, value_size)
+ while len(self.t1) + len(self.b1) >= self.c and self.b1:
+ self.b1.pop()
+
+ total = len(self.t1) + len(self.b1) + len(self.t2) + len(self.b2)
+ while total >= (2 * self.c) and self.b2:
+ self.b2.pop()
+ total -= 1
+ # Finally, move it to MRU position in T1.
+ self.t1.appendleft(key)
+ return
+
+ def _insert(self, trace_record, key, hash, value_size):
+ self.table[key] = CacheEntry(
+ value_size,
+ trace_record.cf_id,
+ trace_record.level,
+ trace_record.block_type,
+ trace_record.table_id,
+ 0,
+ trace_record.access_time,
+ )
+
+ def _should_admit(self, trace_record, key, hash, value_size):
+ return True
+
+ def cache_name(self):
+ if self.enable_cache_row_key:
+ return "Hybrid Adaptive Replacement Cache (arc_hybrid)"
+ return "Adaptive Replacement Cache (arc)"
+
+
+class LRUCache(Cache):
+ """
+ A strict LRU queue.
+ """
+
+ def __init__(self, cache_size, enable_cache_row_key):
+ super(LRUCache, self).__init__(cache_size, enable_cache_row_key)
+ self.table = {}
+ self.lru = Deque()
+
+ def cache_name(self):
+ if self.enable_cache_row_key:
+ return "Hybrid LRU (lru_hybrid)"
+ return "LRU (lru)"
+
+ def _lookup(self, trace_record, key, hash):
+ if key not in self.table:
+ return False
+ # A cache hit. Update LRU queue.
+ self.lru.remove(key)
+ self.lru.appendleft(key)
+ return True
+
+ def _evict(self, trace_record, key, hash, value_size):
+ while self.used_size + value_size > self.cache_size:
+ evict_key = self.lru.pop()
+ self.used_size -= self.table[evict_key].value_size
+ del self.table[evict_key]
+
+ def _insert(self, trace_record, key, hash, value_size):
+ self.table[key] = CacheEntry(
+ value_size,
+ trace_record.cf_id,
+ trace_record.level,
+ trace_record.block_type,
+ trace_record.table_id,
+ 0,
+ trace_record.access_time,
+ )
+ self.lru.appendleft(key)
+
+ def _should_admit(self, trace_record, key, hash, value_size):
+ return True
+
+
+class TraceCache(Cache):
+ """
+ A trace cache. Lookup returns true if the trace observes a cache hit.
+ It is used to maintain cache hits observed in the trace.
+ """
+
+ def __init__(self, cache_size):
+ super(TraceCache, self).__init__(cache_size, enable_cache_row_key=0)
+
+ def _lookup(self, trace_record, key, hash):
+ return trace_record.is_hit
+
+ def _evict(self, trace_record, key, hash, value_size):
+ pass
+
+ def _insert(self, trace_record, key, hash, value_size):
+ pass
+
+ def _should_admit(self, trace_record, key, hash, value_size):
+ return False
+
+ def cache_name(self):
+ return "Trace"
+
+
+def parse_cache_size(cs):
+ cs = cs.replace("\n", "")
+ if cs[-1] == "M":
+ return int(cs[: len(cs) - 1]) * 1024 * 1024
+ if cs[-1] == "G":
+ return int(cs[: len(cs) - 1]) * 1024 * 1024 * 1024
+ if cs[-1] == "T":
+ return int(cs[: len(cs) - 1]) * 1024 * 1024 * 1024 * 1024
+ return int(cs)
+
+
+def create_cache(cache_type, cache_size, downsample_size):
+ cache_size = cache_size / downsample_size
+ enable_cache_row_key = 0
+ if "hybridn" in cache_type:
+ enable_cache_row_key = 2
+ cache_type = cache_type[:-8]
+ if "hybrid" in cache_type:
+ enable_cache_row_key = 1
+ cache_type = cache_type[:-7]
+ if cache_type == "ts":
+ return ThompsonSamplingCache(
+ cache_size,
+ enable_cache_row_key,
+ [LRUPolicy(), LFUPolicy(), HyperbolicPolicy()],
+ cost_class_label=None,
+ )
+ elif cache_type == "linucb":
+ return LinUCBCache(
+ cache_size,
+ enable_cache_row_key,
+ [LRUPolicy(), LFUPolicy(), HyperbolicPolicy()],
+ cost_class_label=None,
+ )
+ elif cache_type == "pylru":
+ return ThompsonSamplingCache(
+ cache_size, enable_cache_row_key, [LRUPolicy()], cost_class_label=None
+ )
+ elif cache_type == "pymru":
+ return ThompsonSamplingCache(
+ cache_size, enable_cache_row_key, [MRUPolicy()], cost_class_label=None
+ )
+ elif cache_type == "pylfu":
+ return ThompsonSamplingCache(
+ cache_size, enable_cache_row_key, [LFUPolicy()], cost_class_label=None
+ )
+ elif cache_type == "pyhb":
+ return ThompsonSamplingCache(
+ cache_size,
+ enable_cache_row_key,
+ [HyperbolicPolicy()],
+ cost_class_label=None,
+ )
+ elif cache_type == "pycctbbt":
+ return ThompsonSamplingCache(
+ cache_size,
+ enable_cache_row_key,
+ [CostClassPolicy()],
+ cost_class_label="table_bt",
+ )
+ elif cache_type == "pycccf":
+ return ThompsonSamplingCache(
+ cache_size, enable_cache_row_key, [CostClassPolicy()], cost_class_label="cf"
+ )
+ elif cache_type == "pycctblevelbt":
+ return ThompsonSamplingCache(
+ cache_size,
+ enable_cache_row_key,
+ [CostClassPolicy()],
+ cost_class_label="table_level_bt",
+ )
+ elif cache_type == "pycccfbt":
+ return ThompsonSamplingCache(
+ cache_size,
+ enable_cache_row_key,
+ [CostClassPolicy()],
+ cost_class_label="cf_bt",
+ )
+ elif cache_type == "pycctb":
+ return ThompsonSamplingCache(
+ cache_size,
+ enable_cache_row_key,
+ [CostClassPolicy()],
+ cost_class_label="table",
+ )
+ elif cache_type == "pyccbt":
+ return ThompsonSamplingCache(
+ cache_size, enable_cache_row_key, [CostClassPolicy()], cost_class_label="bt"
+ )
+ elif cache_type == "opt":
+ if enable_cache_row_key:
+ print("opt does not support hybrid mode.")
+ assert False
+ return OPTCache(cache_size)
+ elif cache_type == "trace":
+ if enable_cache_row_key:
+ print("trace does not support hybrid mode.")
+ assert False
+ return TraceCache(cache_size)
+ elif cache_type == "lru":
+ return LRUCache(cache_size, enable_cache_row_key)
+ elif cache_type == "arc":
+ return ARCCache(cache_size, enable_cache_row_key)
+ elif cache_type == "gdsize":
+ return GDSizeCache(cache_size, enable_cache_row_key)
+ else:
+ print("Unknown cache type {}".format(cache_type))
+ assert False
+ return None
+
+
+class BlockAccessTimeline:
+ """
+ BlockAccessTimeline stores all accesses of a block.
+ """
+
+ def __init__(self):
+ self.accesses = []
+ self.current_access_index = 1
+
+ def get_next_access(self):
+ if self.current_access_index == len(self.accesses):
+ return sys.maxsize
+ next_access_seq_no = self.accesses[self.current_access_index]
+ self.current_access_index += 1
+ return next_access_seq_no
+
+
+def percent(e1, e2):
+ if e2 == 0:
+ return -1
+ return float(e1) * 100.0 / float(e2)
+
+
+def is_target_cf(access_cf, target_cf_name):
+ if target_cf_name == "all":
+ return True
+ return access_cf == target_cf_name
+
+
+def run(
+ trace_file_path,
+ cache_type,
+ cache,
+ warmup_seconds,
+ max_accesses_to_process,
+ target_cf_name,
+):
+ warmup_complete = False
+ trace_miss_ratio_stats = MissRatioStats(kSecondsInMinute)
+ access_seq_no = 0
+ time_interval = 1
+ start_time = time.time()
+ trace_start_time = 0
+ trace_duration = 0
+ is_opt_cache = False
+ if cache.cache_name() == "Belady MIN (opt)":
+ is_opt_cache = True
+
+ block_access_timelines = {}
+ num_no_inserts = 0
+ num_blocks_with_no_size = 0
+ num_inserts_block_with_no_size = 0
+
+ if is_opt_cache:
+ # Read all blocks in memory and stores their access times so that OPT
+ # can use this information to evict the cached key which next access is
+ # the furthest in the future.
+ print("Preprocessing block traces.")
+ with open(trace_file_path, "r") as trace_file:
+ for line in trace_file:
+ if (
+ max_accesses_to_process != -1
+ and access_seq_no > max_accesses_to_process
+ ):
+ break
+ ts = line.split(",")
+ timestamp = int(ts[0])
+ cf_name = ts[5]
+ if not is_target_cf(cf_name, target_cf_name):
+ continue
+ if trace_start_time == 0:
+ trace_start_time = timestamp
+ trace_duration = timestamp - trace_start_time
+ block_id = int(ts[1])
+ block_size = int(ts[3])
+ no_insert = int(ts[9])
+ if block_id not in block_access_timelines:
+ block_access_timelines[block_id] = BlockAccessTimeline()
+ if block_size == 0:
+ num_blocks_with_no_size += 1
+ block_access_timelines[block_id].accesses.append(access_seq_no)
+ access_seq_no += 1
+ if no_insert == 1:
+ num_no_inserts += 1
+ if no_insert == 0 and block_size == 0:
+ num_inserts_block_with_no_size += 1
+ if access_seq_no % 100 != 0:
+ continue
+ now = time.time()
+ if now - start_time > time_interval * 10:
+ print(
+ "Take {} seconds to process {} trace records with trace "
+ "duration of {} seconds. Throughput: {} records/second.".format(
+ now - start_time,
+ access_seq_no,
+ trace_duration / 1000000,
+ access_seq_no / (now - start_time),
+ )
+ )
+ time_interval += 1
+ print(
+ "Trace contains {0} blocks, {1}({2:.2f}%) blocks with no size."
+ "{3} accesses, {4}({5:.2f}%) accesses with no_insert,"
+ "{6}({7:.2f}%) accesses that want to insert but block size is 0.".format(
+ len(block_access_timelines),
+ num_blocks_with_no_size,
+ percent(num_blocks_with_no_size, len(block_access_timelines)),
+ access_seq_no,
+ num_no_inserts,
+ percent(num_no_inserts, access_seq_no),
+ num_inserts_block_with_no_size,
+ percent(num_inserts_block_with_no_size, access_seq_no),
+ )
+ )
+
+ access_seq_no = 0
+ time_interval = 1
+ start_time = time.time()
+ trace_start_time = 0
+ trace_duration = 0
+ print("Running simulated {} cache on block traces.".format(cache.cache_name()))
+ with open(trace_file_path, "r") as trace_file:
+ for line in trace_file:
+ if (
+ max_accesses_to_process != -1
+ and access_seq_no > max_accesses_to_process
+ ):
+ break
+ if access_seq_no % 1000000 == 0:
+ # Force a python gc periodically to reduce memory usage.
+ gc.collect()
+ ts = line.split(",")
+ timestamp = int(ts[0])
+ cf_name = ts[5]
+ if not is_target_cf(cf_name, target_cf_name):
+ continue
+ if trace_start_time == 0:
+ trace_start_time = timestamp
+ trace_duration = timestamp - trace_start_time
+ if (
+ not warmup_complete
+ and warmup_seconds > 0
+ and trace_duration > warmup_seconds * 1000000
+ ):
+ cache.miss_ratio_stats.reset_counter()
+ warmup_complete = True
+ next_access_seq_no = 0
+ block_id = int(ts[1])
+ if is_opt_cache:
+ next_access_seq_no = block_access_timelines[block_id].get_next_access()
+ record = TraceRecord(
+ access_time=int(ts[0]),
+ block_id=int(ts[1]),
+ block_type=int(ts[2]),
+ block_size=int(ts[3]),
+ cf_id=int(ts[4]),
+ cf_name=ts[5],
+ level=int(ts[6]),
+ fd=int(ts[7]),
+ caller=int(ts[8]),
+ no_insert=int(ts[9]),
+ get_id=int(ts[10]),
+ key_id=int(ts[11]),
+ kv_size=int(ts[12]),
+ is_hit=int(ts[13]),
+ referenced_key_exist_in_block=int(ts[14]),
+ num_keys_in_block=int(ts[15]),
+ table_id=int(ts[16]),
+ seq_number=int(ts[17]),
+ block_key_size=int(ts[18]),
+ key_size=int(ts[19]),
+ block_offset_in_file=int(ts[20]),
+ next_access_seq_no=next_access_seq_no,
+ )
+ trace_miss_ratio_stats.update_metrics(
+ record.access_time, is_hit=record.is_hit, miss_bytes=record.block_size
+ )
+ cache.access(record)
+ access_seq_no += 1
+ del record
+ del ts
+ if access_seq_no % 100 != 0:
+ continue
+ # Report progress every 10 seconds.
+ now = time.time()
+ if now - start_time > time_interval * 10:
+ print(
+ "Take {} seconds to process {} trace records with trace "
+ "duration of {} seconds. Throughput: {} records/second. "
+ "Trace miss ratio {}".format(
+ now - start_time,
+ access_seq_no,
+ trace_duration / 1000000,
+ access_seq_no / (now - start_time),
+ trace_miss_ratio_stats.miss_ratio(),
+ )
+ )
+ time_interval += 1
+ print(
+ "{},0,0,{},{},{}".format(
+ cache_type,
+ cache.cache_size,
+ cache.miss_ratio_stats.miss_ratio(),
+ cache.miss_ratio_stats.num_accesses,
+ )
+ )
+ now = time.time()
+ print(
+ "Take {} seconds to process {} trace records with trace duration of {} "
+ "seconds. Throughput: {} records/second. Trace miss ratio {}".format(
+ now - start_time,
+ access_seq_no,
+ trace_duration / 1000000,
+ access_seq_no / (now - start_time),
+ trace_miss_ratio_stats.miss_ratio(),
+ )
+ )
+ print(
+ "{},0,0,{},{},{}".format(
+ cache_type,
+ cache.cache_size,
+ cache.miss_ratio_stats.miss_ratio(),
+ cache.miss_ratio_stats.num_accesses,
+ )
+ )
+ return trace_start_time, trace_duration
+
+
+def report_stats(
+ cache,
+ cache_type,
+ cache_size,
+ target_cf_name,
+ result_dir,
+ trace_start_time,
+ trace_end_time,
+):
+ cache_label = "{}-{}-{}".format(cache_type, cache_size, target_cf_name)
+ with open("{}/data-ml-mrc-{}".format(result_dir, cache_label), "w+") as mrc_file:
+ mrc_file.write(
+ "{},0,0,{},{},{}\n".format(
+ cache_type,
+ cache_size,
+ cache.miss_ratio_stats.miss_ratio(),
+ cache.miss_ratio_stats.num_accesses,
+ )
+ )
+
+ cache_stats = [
+ cache.per_second_miss_ratio_stats,
+ cache.miss_ratio_stats,
+ cache.per_hour_miss_ratio_stats,
+ ]
+ for i in range(len(cache_stats)):
+ avg_miss_bytes, p95_miss_bytes = cache_stats[i].compute_miss_bytes()
+
+ with open(
+ "{}/data-ml-avgmb-{}-{}".format(
+ result_dir, cache_stats[i].time_unit, cache_label
+ ),
+ "w+",
+ ) as mb_file:
+ mb_file.write(
+ "{},0,0,{},{}\n".format(cache_type, cache_size, avg_miss_bytes)
+ )
+
+ with open(
+ "{}/data-ml-p95mb-{}-{}".format(
+ result_dir, cache_stats[i].time_unit, cache_label
+ ),
+ "w+",
+ ) as mb_file:
+ mb_file.write(
+ "{},0,0,{},{}\n".format(cache_type, cache_size, p95_miss_bytes)
+ )
+
+ cache_stats[i].write_miss_timeline(
+ cache_type,
+ cache_size,
+ target_cf_name,
+ result_dir,
+ trace_start_time,
+ trace_end_time,
+ )
+ cache_stats[i].write_miss_ratio_timeline(
+ cache_type,
+ cache_size,
+ target_cf_name,
+ result_dir,
+ trace_start_time,
+ trace_end_time,
+ )
+
+ if not cache.is_ml_cache():
+ return
+
+ policy_stats = [cache.policy_stats, cache.per_hour_policy_stats]
+ for i in range(len(policy_stats)):
+ policy_stats[i].write_policy_timeline(
+ cache_type,
+ cache_size,
+ target_cf_name,
+ result_dir,
+ trace_start_time,
+ trace_end_time,
+ )
+ policy_stats[i].write_policy_ratio_timeline(
+ cache_type,
+ cache_size,
+ target_cf_name,
+ result_dir,
+ trace_start_time,
+ trace_end_time,
+ )
+
+
+if __name__ == "__main__":
+ if len(sys.argv) <= 8:
+ print(
+ "Must provide 8 arguments.\n"
+ "1) Cache type (ts, linucb, arc, lru, opt, pylru, pymru, pylfu, "
+ "pyhb, gdsize, trace). One may evaluate the hybrid row_block cache "
+ "by appending '_hybrid' to a cache_type, e.g., ts_hybrid. "
+ "Note that hybrid is not supported with opt and trace. \n"
+ "2) Cache size (xM, xG, xT).\n"
+ "3) The sampling frequency used to collect the trace. (The "
+ "simulation scales down the cache size by the sampling frequency).\n"
+ "4) Warmup seconds (The number of seconds used for warmup).\n"
+ "5) Trace file path.\n"
+ "6) Result directory (A directory that saves generated results)\n"
+ "7) Max number of accesses to process\n"
+ "8) The target column family. (The simulation will only run "
+ "accesses on the target column family. If it is set to all, "
+ "it will run against all accesses.)"
+ )
+ exit(1)
+ print("Arguments: {}".format(sys.argv))
+ cache_type = sys.argv[1]
+ cache_size = parse_cache_size(sys.argv[2])
+ downsample_size = int(sys.argv[3])
+ warmup_seconds = int(sys.argv[4])
+ trace_file_path = sys.argv[5]
+ result_dir = sys.argv[6]
+ max_accesses_to_process = int(sys.argv[7])
+ target_cf_name = sys.argv[8]
+ cache = create_cache(cache_type, cache_size, downsample_size)
+ trace_start_time, trace_duration = run(
+ trace_file_path,
+ cache_type,
+ cache,
+ warmup_seconds,
+ max_accesses_to_process,
+ target_cf_name,
+ )
+ trace_end_time = trace_start_time + trace_duration
+ report_stats(
+ cache,
+ cache_type,
+ cache_size,
+ target_cf_name,
+ result_dir,
+ trace_start_time,
+ trace_end_time,
+ )
diff --git a/src/rocksdb/tools/block_cache_analyzer/block_cache_pysim.sh b/src/rocksdb/tools/block_cache_analyzer/block_cache_pysim.sh
new file mode 100644
index 000000000..295f734aa
--- /dev/null
+++ b/src/rocksdb/tools/block_cache_analyzer/block_cache_pysim.sh
@@ -0,0 +1,156 @@
+#!/usr/bin/env bash
+# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
+#
+# A shell script to run a batch of pysims and combine individual pysim output files.
+#
+# Usage: bash block_cache_pysim.sh trace_file_path result_dir downsample_size warmup_seconds max_jobs
+# trace_file_path: The file path that stores the traces.
+# result_dir: The directory to store pysim results. The output files from a pysim is stores in result_dir/ml
+# downsample_size: The downsample size used to collect the trace.
+# warmup_seconds: The number of seconds used for warmup.
+# max_jobs: The max number of concurrent pysims to run.
+
+# Install required packages to run simulations.
+# sudo dnf install -y numpy scipy python-matplotlib ipython python-pandas sympy python-nose atlas-devel
+ulimit -c 0
+
+if [ $# -ne 5 ]; then
+ echo "Usage: ./block_cache_pysim.sh trace_file_path result_dir downsample_size warmup_seconds max_jobs"
+ exit 0
+fi
+
+trace_file="$1"
+result_dir="$2"
+downsample_size="$3"
+warmup_seconds="$4"
+max_jobs="$5"
+max_num_accesses=100000000
+current_jobs=1
+
+ml_tmp_result_dir="$result_dir/ml"
+rm -rf "$ml_tmp_result_dir"
+mkdir -p "$result_dir"
+mkdir -p "$ml_tmp_result_dir"
+
+# Report miss ratio in the trace.
+current_jobs=$(ps aux | grep pysim | grep python | grep -cv grep)
+for cf_name in "all"
+do
+for cache_size in "1G" "2G" "4G" "8G" "16G" #"12G" "16G" "1T"
+do
+for cache_type in "opt" "lru" "pylru" "pycctbbt" "pyhb" "ts" "trace" "lru_hybrid" #"pycctblevelbt" #"lru_hybridn" "opt" #"pylru" "pylru_hybrid" "pycctbbt" "pycccfbt" "trace"
+do
+ if [[ $cache_type == "trace" && $cache_size != "16G" ]]; then
+ # We only need to collect miss ratios observed in the trace once.
+ continue
+ fi
+ while [ "$current_jobs" -ge "$max_jobs" ]
+ do
+ sleep 10
+ echo "Waiting jobs to complete. Number of running jobs: $current_jobs"
+ current_jobs=$(ps aux | grep pysim | grep python | grep -cv grep)
+ echo "Waiting jobs to complete. Number of running jobs: $current_jobs"
+ done
+ output="log-ml-$cache_type-$cache_size-$cf_name"
+ echo "Running simulation for $cache_type, cache size $cache_size, and cf_name $cf_name. Number of running jobs: $current_jobs. "
+ nohup python block_cache_pysim.py "$cache_type" "$cache_size" "$downsample_size" "$warmup_seconds" "$trace_file" "$ml_tmp_result_dir" "$max_num_accesses" "$cf_name" >& "$ml_tmp_result_dir/$output" &
+ current_jobs=$((current_jobs+1))
+done
+done
+done
+
+# Wait for all jobs to complete.
+while [ $current_jobs -gt 0 ]
+do
+ sleep 10
+ echo "Waiting jobs to complete. Number of running jobs: $current_jobs"
+ current_jobs=$(ps aux | grep pysim | grep python | grep -cv grep)
+ echo "Waiting jobs to complete. Number of running jobs: $current_jobs"
+done
+
+echo "Combine individual pysim output files"
+
+rm -rf "$result_dir/ml_*"
+for header in "header-" "data-"
+do
+for fn in "$ml_tmp_result_dir"/*
+do
+ sum_file=""
+ time_unit=""
+ capacity=""
+ target_cf_name=""
+ if [[ $fn == *"timeline"* ]]; then
+ tmpfn="$fn"
+ IFS='-' read -ra elements <<< "$tmpfn"
+ time_unit_index=0
+ capacity_index=0
+ for i in "${elements[@]}"
+ do
+ if [[ $i == "timeline" ]]; then
+ break
+ fi
+ time_unit_index=$((time_unit_index+1))
+ done
+ time_unit_index=$((time_unit_index+1))
+ capacity_index=$((time_unit_index+2))
+ target_cf_name_index=$((time_unit_index+3))
+ time_unit="${elements[$time_unit_index]}_"
+ capacity="${elements[$capacity_index]}_"
+ target_cf_name="${elements[$target_cf_name_index]}_"
+ fi
+
+ if [[ $fn == *"${header}ml-policy-timeline"* ]]; then
+ sum_file="$result_dir/ml_${target_cf_name}${capacity}${time_unit}policy_timeline"
+ fi
+ if [[ $fn == *"${header}ml-policy-ratio-timeline"* ]]; then
+ sum_file="$result_dir/ml_${target_cf_name}${capacity}${time_unit}policy_ratio_timeline"
+ fi
+ if [[ $fn == *"${header}ml-miss-timeline"* ]]; then
+ sum_file="$result_dir/ml_${target_cf_name}${capacity}${time_unit}miss_timeline"
+ fi
+ if [[ $fn == *"${header}ml-miss-ratio-timeline"* ]]; then
+ sum_file="$result_dir/ml_${target_cf_name}${capacity}${time_unit}miss_ratio_timeline"
+ fi
+ if [[ $fn == *"${header}ml-mrc"* ]]; then
+ tmpfn="$fn"
+ IFS='-' read -ra elements <<< "$tmpfn"
+ target_cf_name=${elements[-1]}
+ sum_file="${result_dir}/ml_${target_cf_name}_mrc"
+ fi
+ if [[ $fn == *"${header}ml-avgmb"* ]]; then
+ tmpfn="$fn"
+ IFS='-' read -ra elements <<< "$tmpfn"
+ time_unit=${elements[3]}
+ target_cf_name=${elements[-1]}
+ sum_file="${result_dir}/ml_${time_unit}_${target_cf_name}_avgmb"
+ fi
+ if [[ $fn == *"${header}ml-p95mb"* ]]; then
+ tmpfn="$fn"
+ IFS='-' read -ra elements <<< "$tmpfn"
+ time_unit=${elements[3]}
+ target_cf_name=${elements[-1]}
+ sum_file="${result_dir}/ml_${time_unit}_${target_cf_name}_p95mb"
+ fi
+ if [[ $sum_file == "" ]]; then
+ continue
+ fi
+ if [[ $header == "header-" ]]; then
+ if [ -e "$sum_file" ]; then
+ continue
+ fi
+ fi
+ cat "$fn" >> "$sum_file"
+done
+done
+
+echo "Done"
+for fn in $result_dir/*
+do
+ if [[ $fn == *"_mrc" || $fn == *"_avgmb" || $fn == *"_p95mb" ]]; then
+ # Sort MRC file by cache_type and cache_size.
+ tmp_file="$result_dir/tmp_mrc"
+ cat "$fn" | sort -t ',' -k1,1 -k4,4n > "$tmp_file"
+ cat "$tmp_file" > "$fn"
+ rm -rf "$tmp_file"
+ fi
+done
diff --git a/src/rocksdb/tools/block_cache_analyzer/block_cache_pysim_test.py b/src/rocksdb/tools/block_cache_analyzer/block_cache_pysim_test.py
new file mode 100644
index 000000000..eed1b94af
--- /dev/null
+++ b/src/rocksdb/tools/block_cache_analyzer/block_cache_pysim_test.py
@@ -0,0 +1,734 @@
+#!/usr/bin/env python3
+# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
+
+import os
+import random
+import sys
+
+from block_cache_pysim import (
+ ARCCache,
+ CacheEntry,
+ create_cache,
+ GDSizeCache,
+ HashTable,
+ HyperbolicPolicy,
+ kMicrosInSecond,
+ kSampleSize,
+ LFUPolicy,
+ LinUCBCache,
+ LRUCache,
+ LRUPolicy,
+ MRUPolicy,
+ OPTCache,
+ OPTCacheEntry,
+ run,
+ ThompsonSamplingCache,
+ TraceCache,
+ TraceRecord,
+)
+
+
+def test_hash_table():
+ print("Test hash table")
+ table = HashTable()
+ data_size = 10000
+ for i in range(data_size):
+ table.insert("k{}".format(i), i, "v{}".format(i))
+ for i in range(data_size):
+ assert table.lookup("k{}".format(i), i) is not None
+ for i in range(data_size):
+ table.delete("k{}".format(i), i)
+ for i in range(data_size):
+ assert table.lookup("k{}".format(i), i) is None
+
+ truth_map = {}
+ n = 1000000
+ records = 100
+ for i in range(n):
+ key_id = random.randint(0, records)
+ v = random.randint(0, records)
+ key = "k{}".format(key_id)
+ value = CacheEntry(v, v, v, v, v, v, v)
+ action = random.randint(0, 10)
+ assert len(truth_map) == table.elements, "{} {} {}".format(
+ len(truth_map), table.elements, i
+ )
+ if action <= 8:
+ if key in truth_map:
+ assert table.lookup(key, key_id) is not None
+ assert truth_map[key].value_size == table.lookup(key, key_id).value_size
+ else:
+ assert table.lookup(key, key_id) is None
+ table.insert(key, key_id, value)
+ truth_map[key] = value
+ else:
+ deleted = table.delete(key, key_id)
+ if deleted:
+ assert key in truth_map
+ if key in truth_map:
+ del truth_map[key]
+
+ # Check all keys are unique in the sample set.
+ for _i in range(10):
+ samples = table.random_sample(kSampleSize)
+ unique_keys = {}
+ for sample in samples:
+ unique_keys[sample.key] = True
+ assert len(samples) == len(unique_keys)
+
+ assert len(table) == len(truth_map)
+ for key in truth_map:
+ assert table.lookup(key, int(key[1:])) is not None
+ assert truth_map[key].value_size == table.lookup(key, int(key[1:])).value_size
+ print("Test hash table: Success")
+
+
+def assert_metrics(cache, expected_value, expected_value_size=1, custom_hashtable=True):
+ assert cache.used_size == expected_value[0], "Expected {}, Actual {}".format(
+ expected_value[0], cache.used_size
+ )
+ assert (
+ cache.miss_ratio_stats.num_accesses == expected_value[1]
+ ), "Expected {}, Actual {}".format(
+ expected_value[1], cache.miss_ratio_stats.num_accesses
+ )
+ assert (
+ cache.miss_ratio_stats.num_misses == expected_value[2]
+ ), "Expected {}, Actual {}".format(
+ expected_value[2], cache.miss_ratio_stats.num_misses
+ )
+ assert len(cache.table) == len(expected_value[3]) + len(
+ expected_value[4]
+ ), "Expected {}, Actual {}".format(
+ len(expected_value[3]) + len(expected_value[4]), cache.table.elements
+ )
+ for expeceted_k in expected_value[3]:
+ if custom_hashtable:
+ val = cache.table.lookup("b{}".format(expeceted_k), expeceted_k)
+ else:
+ val = cache.table["b{}".format(expeceted_k)]
+ assert val is not None, "Expected {} Actual: Not Exist {}, Table: {}".format(
+ expeceted_k, expected_value, cache.table
+ )
+ assert val.value_size == expected_value_size
+ for expeceted_k in expected_value[4]:
+ if custom_hashtable:
+ val = cache.table.lookup("g0-{}".format(expeceted_k), expeceted_k)
+ else:
+ val = cache.table["g0-{}".format(expeceted_k)]
+ assert val is not None
+ assert val.value_size == expected_value_size
+
+
+# Access k1, k1, k2, k3, k3, k3, k4
+# When k4 is inserted,
+# LRU should evict k1.
+# LFU should evict k2.
+# MRU should evict k3.
+def test_cache(cache, expected_value, custom_hashtable=True):
+ k1 = TraceRecord(
+ access_time=0,
+ block_id=1,
+ block_type=1,
+ block_size=1,
+ cf_id=0,
+ cf_name="",
+ level=0,
+ fd=0,
+ caller=1,
+ no_insert=0,
+ get_id=1,
+ key_id=1,
+ kv_size=5,
+ is_hit=1,
+ referenced_key_exist_in_block=1,
+ num_keys_in_block=0,
+ table_id=0,
+ seq_number=0,
+ block_key_size=0,
+ key_size=0,
+ block_offset_in_file=0,
+ next_access_seq_no=0,
+ )
+ k2 = TraceRecord(
+ access_time=1,
+ block_id=2,
+ block_type=1,
+ block_size=1,
+ cf_id=0,
+ cf_name="",
+ level=0,
+ fd=0,
+ caller=1,
+ no_insert=0,
+ get_id=1,
+ key_id=1,
+ kv_size=5,
+ is_hit=1,
+ referenced_key_exist_in_block=1,
+ num_keys_in_block=0,
+ table_id=0,
+ seq_number=0,
+ block_key_size=0,
+ key_size=0,
+ block_offset_in_file=0,
+ next_access_seq_no=0,
+ )
+ k3 = TraceRecord(
+ access_time=2,
+ block_id=3,
+ block_type=1,
+ block_size=1,
+ cf_id=0,
+ cf_name="",
+ level=0,
+ fd=0,
+ caller=1,
+ no_insert=0,
+ get_id=1,
+ key_id=1,
+ kv_size=5,
+ is_hit=1,
+ referenced_key_exist_in_block=1,
+ num_keys_in_block=0,
+ table_id=0,
+ seq_number=0,
+ block_key_size=0,
+ key_size=0,
+ block_offset_in_file=0,
+ next_access_seq_no=0,
+ )
+ k4 = TraceRecord(
+ access_time=3,
+ block_id=4,
+ block_type=1,
+ block_size=1,
+ cf_id=0,
+ cf_name="",
+ level=0,
+ fd=0,
+ caller=1,
+ no_insert=0,
+ get_id=1,
+ key_id=1,
+ kv_size=5,
+ is_hit=1,
+ referenced_key_exist_in_block=1,
+ num_keys_in_block=0,
+ table_id=0,
+ seq_number=0,
+ block_key_size=0,
+ key_size=0,
+ block_offset_in_file=0,
+ next_access_seq_no=0,
+ )
+ sequence = [k1, k1, k2, k3, k3, k3]
+ index = 0
+ expected_values = []
+ # Access k1, miss.
+ expected_values.append([1, 1, 1, [1], []])
+ # Access k1, hit.
+ expected_values.append([1, 2, 1, [1], []])
+ # Access k2, miss.
+ expected_values.append([2, 3, 2, [1, 2], []])
+ # Access k3, miss.
+ expected_values.append([3, 4, 3, [1, 2, 3], []])
+ # Access k3, hit.
+ expected_values.append([3, 5, 3, [1, 2, 3], []])
+ # Access k3, hit.
+ expected_values.append([3, 6, 3, [1, 2, 3], []])
+ access_time = 0
+ for access in sequence:
+ access.access_time = access_time
+ cache.access(access)
+ assert_metrics(
+ cache,
+ expected_values[index],
+ expected_value_size=1,
+ custom_hashtable=custom_hashtable,
+ )
+ access_time += 1
+ index += 1
+ k4.access_time = access_time
+ cache.access(k4)
+ assert_metrics(
+ cache, expected_value, expected_value_size=1, custom_hashtable=custom_hashtable
+ )
+
+
+def test_lru_cache(cache, custom_hashtable):
+ print("Test LRU cache")
+ # Access k4, miss. evict k1
+ test_cache(cache, [3, 7, 4, [2, 3, 4], []], custom_hashtable)
+ print("Test LRU cache: Success")
+
+
+def test_mru_cache():
+ print("Test MRU cache")
+ policies = []
+ policies.append(MRUPolicy())
+ # Access k4, miss. evict k3
+ test_cache(
+ ThompsonSamplingCache(3, False, policies, cost_class_label=None),
+ [3, 7, 4, [1, 2, 4], []],
+ )
+ print("Test MRU cache: Success")
+
+
+def test_lfu_cache():
+ print("Test LFU cache")
+ policies = []
+ policies.append(LFUPolicy())
+ # Access k4, miss. evict k2
+ test_cache(
+ ThompsonSamplingCache(3, False, policies, cost_class_label=None),
+ [3, 7, 4, [1, 3, 4], []],
+ )
+ print("Test LFU cache: Success")
+
+
+def test_mix(cache):
+ print("Test Mix {} cache".format(cache.cache_name()))
+ n = 100000
+ records = 100
+ block_size_table = {}
+ trace_num_misses = 0
+ for i in range(n):
+ key_id = random.randint(0, records)
+ vs = random.randint(0, 10)
+ now = i * kMicrosInSecond
+ block_size = vs
+ if key_id in block_size_table:
+ block_size = block_size_table[key_id]
+ else:
+ block_size_table[key_id] = block_size
+ is_hit = key_id % 2
+ if is_hit == 0:
+ trace_num_misses += 1
+ k = TraceRecord(
+ access_time=now,
+ block_id=key_id,
+ block_type=1,
+ block_size=block_size,
+ cf_id=0,
+ cf_name="",
+ level=0,
+ fd=0,
+ caller=1,
+ no_insert=0,
+ get_id=key_id,
+ key_id=key_id,
+ kv_size=5,
+ is_hit=is_hit,
+ referenced_key_exist_in_block=1,
+ num_keys_in_block=0,
+ table_id=0,
+ seq_number=0,
+ block_key_size=0,
+ key_size=0,
+ block_offset_in_file=0,
+ next_access_seq_no=vs,
+ )
+ cache.access(k)
+ assert cache.miss_ratio_stats.miss_ratio() > 0
+ if cache.cache_name() == "Trace":
+ assert cache.miss_ratio_stats.num_accesses == n
+ assert cache.miss_ratio_stats.num_misses == trace_num_misses
+ else:
+ assert cache.used_size <= cache.cache_size
+ all_values = cache.table.values()
+ cached_size = 0
+ for value in all_values:
+ cached_size += value.value_size
+ assert cached_size == cache.used_size, "Expeced {} Actual {}".format(
+ cache.used_size, cached_size
+ )
+ print("Test Mix {} cache: Success".format(cache.cache_name()))
+
+
+def test_end_to_end():
+ print("Test All caches")
+ n = 100000
+ nblocks = 1000
+ block_size = 16 * 1024
+ ncfs = 7
+ nlevels = 6
+ nfds = 100000
+ trace_file_path = "test_trace"
+ # All blocks are of the same size so that OPT must achieve the lowest miss
+ # ratio.
+ with open(trace_file_path, "w+") as trace_file:
+ access_records = ""
+ for i in range(n):
+ key_id = random.randint(0, nblocks)
+ cf_id = random.randint(0, ncfs)
+ level = random.randint(0, nlevels)
+ fd = random.randint(0, nfds)
+ now = i * kMicrosInSecond
+ access_record = ""
+ access_record += "{},".format(now)
+ access_record += "{},".format(key_id)
+ access_record += "{},".format(9) # block type
+ access_record += "{},".format(block_size) # block size
+ access_record += "{},".format(cf_id)
+ access_record += "cf_{},".format(cf_id)
+ access_record += "{},".format(level)
+ access_record += "{},".format(fd)
+ access_record += "{},".format(key_id % 3) # caller
+ access_record += "{},".format(0) # no insert
+ access_record += "{},".format(i) # get_id
+ access_record += "{},".format(i) # key_id
+ access_record += "{},".format(100) # kv_size
+ access_record += "{},".format(1) # is_hit
+ access_record += "{},".format(1) # referenced_key_exist_in_block
+ access_record += "{},".format(10) # num_keys_in_block
+ access_record += "{},".format(1) # table_id
+ access_record += "{},".format(0) # seq_number
+ access_record += "{},".format(10) # block key size
+ access_record += "{},".format(20) # key size
+ access_record += "{},".format(0) # block offset
+ access_record = access_record[:-1]
+ access_records += access_record + "\n"
+ trace_file.write(access_records)
+
+ print("Test All caches: Start testing caches")
+ cache_size = block_size * nblocks / 10
+ downsample_size = 1
+ cache_ms = {}
+ for cache_type in [
+ "ts",
+ "opt",
+ "lru",
+ "pylru",
+ "linucb",
+ "gdsize",
+ "pyccbt",
+ "pycctbbt",
+ ]:
+ cache = create_cache(cache_type, cache_size, downsample_size)
+ run(trace_file_path, cache_type, cache, 0, -1, "all")
+ cache_ms[cache_type] = cache
+ assert cache.miss_ratio_stats.num_accesses == n
+
+ for cache_type in cache_ms:
+ cache = cache_ms[cache_type]
+ ms = cache.miss_ratio_stats.miss_ratio()
+ assert ms <= 100.0 and ms >= 0.0
+ # OPT should perform the best.
+ assert cache_ms["opt"].miss_ratio_stats.miss_ratio() <= ms
+ assert cache.used_size <= cache.cache_size
+ all_values = cache.table.values()
+ cached_size = 0
+ for value in all_values:
+ cached_size += value.value_size
+ assert cached_size == cache.used_size, "Expeced {} Actual {}".format(
+ cache.used_size, cached_size
+ )
+ print("Test All {}: Success".format(cache.cache_name()))
+
+ os.remove(trace_file_path)
+ print("Test All: Success")
+
+
+def test_hybrid(cache):
+ print("Test {} cache".format(cache.cache_name()))
+ k = TraceRecord(
+ access_time=0,
+ block_id=1,
+ block_type=1,
+ block_size=1,
+ cf_id=0,
+ cf_name="",
+ level=0,
+ fd=0,
+ caller=1,
+ no_insert=0,
+ get_id=1, # the first get request.
+ key_id=1,
+ kv_size=0, # no size.
+ is_hit=1,
+ referenced_key_exist_in_block=1,
+ num_keys_in_block=0,
+ table_id=0,
+ seq_number=0,
+ block_key_size=0,
+ key_size=0,
+ block_offset_in_file=0,
+ next_access_seq_no=0,
+ )
+ cache.access(k) # Expect a miss.
+ # used size, num accesses, num misses, hash table size, blocks, get keys.
+ assert_metrics(cache, [1, 1, 1, [1], []])
+ k.access_time += 1
+ k.kv_size = 1
+ k.block_id = 2
+ cache.access(k) # k should be inserted.
+ assert_metrics(cache, [3, 2, 2, [1, 2], [1]])
+ k.access_time += 1
+ k.block_id = 3
+ cache.access(k) # k should not be inserted again.
+ assert_metrics(cache, [4, 3, 3, [1, 2, 3], [1]])
+ # A second get request referencing the same key.
+ k.access_time += 1
+ k.get_id = 2
+ k.block_id = 4
+ k.kv_size = 0
+ cache.access(k) # k should observe a hit. No block access.
+ assert_metrics(cache, [4, 4, 3, [1, 2, 3], [1]])
+
+ # A third get request searches three files, three different keys.
+ # And the second key observes a hit.
+ k.access_time += 1
+ k.kv_size = 1
+ k.get_id = 3
+ k.block_id = 3
+ k.key_id = 2
+ cache.access(k) # k should observe a miss. block 3 observes a hit.
+ assert_metrics(cache, [5, 5, 3, [1, 2, 3], [1, 2]])
+
+ k.access_time += 1
+ k.kv_size = 1
+ k.get_id = 3
+ k.block_id = 4
+ k.kv_size = 1
+ k.key_id = 1
+ cache.access(k) # k1 should observe a hit.
+ assert_metrics(cache, [5, 6, 3, [1, 2, 3], [1, 2]])
+
+ k.access_time += 1
+ k.kv_size = 1
+ k.get_id = 3
+ k.block_id = 4
+ k.kv_size = 1
+ k.key_id = 3
+ # k3 should observe a miss.
+ # However, as the get already complete, we should not access k3 any more.
+ cache.access(k)
+ assert_metrics(cache, [5, 7, 3, [1, 2, 3], [1, 2]])
+
+ # A fourth get request searches one file and two blocks. One row key.
+ k.access_time += 1
+ k.get_id = 4
+ k.block_id = 5
+ k.key_id = 4
+ k.kv_size = 1
+ cache.access(k)
+ assert_metrics(cache, [7, 8, 4, [1, 2, 3, 5], [1, 2, 4]])
+
+ # A bunch of insertions which evict cached row keys.
+ for i in range(6, 100):
+ k.access_time += 1
+ k.get_id = 0
+ k.block_id = i
+ cache.access(k)
+
+ k.get_id = 4
+ k.block_id = 100 # A different block.
+ k.key_id = 4 # Same row key and should not be inserted again.
+ k.kv_size = 1
+ cache.access(k)
+ assert_metrics(
+ cache, [kSampleSize, 103, 99, [i for i in range(101 - kSampleSize, 101)], []]
+ )
+ print("Test {} cache: Success".format(cache.cache_name()))
+
+
+def test_opt_cache():
+ print("Test OPT cache")
+ cache = OPTCache(3)
+ # seq: 0, 1, 2, 3, 4, 5, 6, 7, 8
+ # key: k1, k2, k3, k4, k5, k6, k7, k1, k8
+ # next_access: 7, 19, 18, M, M, 17, 16, 25, M
+ k = TraceRecord(
+ access_time=0,
+ block_id=1,
+ block_type=1,
+ block_size=1,
+ cf_id=0,
+ cf_name="",
+ level=0,
+ fd=0,
+ caller=1,
+ no_insert=0,
+ get_id=1, # the first get request.
+ key_id=1,
+ kv_size=0, # no size.
+ is_hit=1,
+ referenced_key_exist_in_block=1,
+ num_keys_in_block=0,
+ table_id=0,
+ seq_number=0,
+ block_key_size=0,
+ key_size=0,
+ block_offset_in_file=0,
+ next_access_seq_no=7,
+ )
+ cache.access(k)
+ assert_metrics(
+ cache, [1, 1, 1, [1], []], expected_value_size=1, custom_hashtable=False
+ )
+ k.access_time += 1
+ k.block_id = 2
+ k.next_access_seq_no = 19
+ cache.access(k)
+ assert_metrics(
+ cache, [2, 2, 2, [1, 2], []], expected_value_size=1, custom_hashtable=False
+ )
+ k.access_time += 1
+ k.block_id = 3
+ k.next_access_seq_no = 18
+ cache.access(k)
+ assert_metrics(
+ cache, [3, 3, 3, [1, 2, 3], []], expected_value_size=1, custom_hashtable=False
+ )
+ k.access_time += 1
+ k.block_id = 4
+ k.next_access_seq_no = sys.maxsize # Never accessed again.
+ cache.access(k)
+ # Evict 2 since its next access 19 is the furthest in the future.
+ assert_metrics(
+ cache, [3, 4, 4, [1, 3, 4], []], expected_value_size=1, custom_hashtable=False
+ )
+ k.access_time += 1
+ k.block_id = 5
+ k.next_access_seq_no = sys.maxsize # Never accessed again.
+ cache.access(k)
+ # Evict 4 since its next access MAXINT is the furthest in the future.
+ assert_metrics(
+ cache, [3, 5, 5, [1, 3, 5], []], expected_value_size=1, custom_hashtable=False
+ )
+ k.access_time += 1
+ k.block_id = 6
+ k.next_access_seq_no = 17
+ cache.access(k)
+ # Evict 5 since its next access MAXINT is the furthest in the future.
+ assert_metrics(
+ cache, [3, 6, 6, [1, 3, 6], []], expected_value_size=1, custom_hashtable=False
+ )
+ k.access_time += 1
+ k.block_id = 7
+ k.next_access_seq_no = 16
+ cache.access(k)
+ # Evict 3 since its next access 18 is the furthest in the future.
+ assert_metrics(
+ cache, [3, 7, 7, [1, 6, 7], []], expected_value_size=1, custom_hashtable=False
+ )
+ k.access_time += 1
+ k.block_id = 1
+ k.next_access_seq_no = 25
+ cache.access(k)
+ assert_metrics(
+ cache, [3, 8, 7, [1, 6, 7], []], expected_value_size=1, custom_hashtable=False
+ )
+ k.access_time += 1
+ k.block_id = 8
+ k.next_access_seq_no = sys.maxsize
+ cache.access(k)
+ # Evict 1 since its next access 25 is the furthest in the future.
+ assert_metrics(
+ cache, [3, 9, 8, [6, 7, 8], []], expected_value_size=1, custom_hashtable=False
+ )
+
+ # Insert a large kv pair to evict all keys.
+ k.access_time += 1
+ k.block_id = 10
+ k.block_size = 3
+ k.next_access_seq_no = sys.maxsize
+ cache.access(k)
+ assert_metrics(
+ cache, [3, 10, 9, [10], []], expected_value_size=3, custom_hashtable=False
+ )
+ print("Test OPT cache: Success")
+
+
+def test_trace_cache():
+ print("Test trace cache")
+ cache = TraceCache(0)
+ k = TraceRecord(
+ access_time=0,
+ block_id=1,
+ block_type=1,
+ block_size=1,
+ cf_id=0,
+ cf_name="",
+ level=0,
+ fd=0,
+ caller=1,
+ no_insert=0,
+ get_id=1,
+ key_id=1,
+ kv_size=0,
+ is_hit=1,
+ referenced_key_exist_in_block=1,
+ num_keys_in_block=0,
+ table_id=0,
+ seq_number=0,
+ block_key_size=0,
+ key_size=0,
+ block_offset_in_file=0,
+ next_access_seq_no=7,
+ )
+ cache.access(k)
+ assert cache.miss_ratio_stats.num_accesses == 1
+ assert cache.miss_ratio_stats.num_misses == 0
+ k.is_hit = 0
+ cache.access(k)
+ assert cache.miss_ratio_stats.num_accesses == 2
+ assert cache.miss_ratio_stats.num_misses == 1
+ print("Test trace cache: Success")
+
+
+if __name__ == "__main__":
+ test_hash_table()
+ test_trace_cache()
+ test_opt_cache()
+ test_lru_cache(
+ ThompsonSamplingCache(
+ 3, enable_cache_row_key=0, policies=[LRUPolicy()], cost_class_label=None
+ ),
+ custom_hashtable=True,
+ )
+ test_lru_cache(LRUCache(3, enable_cache_row_key=0), custom_hashtable=False)
+ test_mru_cache()
+ test_lfu_cache()
+ test_hybrid(
+ ThompsonSamplingCache(
+ kSampleSize,
+ enable_cache_row_key=1,
+ policies=[LRUPolicy()],
+ cost_class_label=None,
+ )
+ )
+ test_hybrid(
+ LinUCBCache(
+ kSampleSize,
+ enable_cache_row_key=1,
+ policies=[LRUPolicy()],
+ cost_class_label=None,
+ )
+ )
+ for cache_type in [
+ "ts",
+ "opt",
+ "arc",
+ "pylfu",
+ "pymru",
+ "trace",
+ "pyhb",
+ "lru",
+ "pylru",
+ "linucb",
+ "gdsize",
+ "pycctbbt",
+ "pycctb",
+ "pyccbt",
+ ]:
+ for enable_row_cache in [0, 1, 2]:
+ cache_type_str = cache_type
+ if cache_type != "opt" and cache_type != "trace":
+ if enable_row_cache == 1:
+ cache_type_str += "_hybrid"
+ elif enable_row_cache == 2:
+ cache_type_str += "_hybridn"
+ test_mix(create_cache(cache_type_str, cache_size=100, downsample_size=1))
+ test_end_to_end()
diff --git a/src/rocksdb/tools/block_cache_analyzer/block_cache_trace_analyzer.cc b/src/rocksdb/tools/block_cache_analyzer/block_cache_trace_analyzer.cc
new file mode 100644
index 000000000..f0bb6975b
--- /dev/null
+++ b/src/rocksdb/tools/block_cache_analyzer/block_cache_trace_analyzer.cc
@@ -0,0 +1,2316 @@
+// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+// This source code is licensed under both the GPLv2 (found in the
+// COPYING file in the root directory) and Apache 2.0 License
+// (found in the LICENSE.Apache file in the root directory).
+
+#ifndef ROCKSDB_LITE
+#ifdef GFLAGS
+#include "tools/block_cache_analyzer/block_cache_trace_analyzer.h"
+
+#include <algorithm>
+#include <cinttypes>
+#include <cstdio>
+#include <cstdlib>
+#include <fstream>
+#include <iomanip>
+#include <iostream>
+#include <memory>
+#include <random>
+#include <sstream>
+
+#include "monitoring/histogram.h"
+#include "rocksdb/system_clock.h"
+#include "rocksdb/trace_record.h"
+#include "util/gflags_compat.h"
+#include "util/string_util.h"
+
+using GFLAGS_NAMESPACE::ParseCommandLineFlags;
+
+DEFINE_string(block_cache_trace_path, "", "The trace file path.");
+DEFINE_bool(is_block_cache_human_readable_trace, false,
+ "Is the trace file provided for analysis generated by running "
+ "block_cache_trace_analyzer with "
+ "FLAGS_human_readable_trace_file_path is specified.");
+DEFINE_string(
+ block_cache_sim_config_path, "",
+ "The config file path. One cache configuration per line. The format of a "
+ "cache configuration is "
+ "cache_name,num_shard_bits,ghost_capacity,cache_capacity_1,...,cache_"
+ "capacity_N. Supported cache names are lru, lru_priority, lru_hybrid, and "
+ "lru_hybrid_no_insert_on_row_miss. User may also add a prefix 'ghost_' to "
+ "a cache_name to add a ghost cache in front of the real cache. "
+ "ghost_capacity and cache_capacity can be xK, xM or xG where x is a "
+ "positive number.");
+DEFINE_int32(block_cache_trace_downsample_ratio, 1,
+ "The trace collected accesses on one in every "
+ "block_cache_trace_downsample_ratio blocks. We scale "
+ "down the simulated cache size by this ratio.");
+DEFINE_bool(print_block_size_stats, false,
+ "Print block size distribution and the distribution break down by "
+ "block type and column family.");
+DEFINE_bool(print_access_count_stats, false,
+ "Print access count distribution and the distribution break down "
+ "by block type and column family.");
+DEFINE_bool(print_data_block_access_count_stats, false,
+ "Print data block accesses by user Get and Multi-Get.");
+DEFINE_int32(cache_sim_warmup_seconds, 0,
+ "The number of seconds to warmup simulated caches. The hit/miss "
+ "counters are reset after the warmup completes.");
+DEFINE_int32(analyze_bottom_k_access_count_blocks, 0,
+ "Print out detailed access information for blocks with their "
+ "number of accesses are the bottom k among all blocks.");
+DEFINE_int32(analyze_top_k_access_count_blocks, 0,
+ "Print out detailed access information for blocks with their "
+ "number of accesses are the top k among all blocks.");
+DEFINE_string(block_cache_analysis_result_dir, "",
+ "The directory that saves block cache analysis results.");
+DEFINE_string(
+ timeline_labels, "",
+ "Group the number of accesses per block per second using these labels. "
+ "Possible labels are a combination of the following: cf (column family), "
+ "sst, level, bt (block type), caller, block. For example, label \"cf_bt\" "
+ "means the number of access per second is grouped by unique pairs of "
+ "\"cf_bt\". A label \"all\" contains the aggregated number of accesses per "
+ "second across all possible labels.");
+DEFINE_string(reuse_distance_labels, "",
+ "Group the reuse distance of a block using these labels. Reuse "
+ "distance is defined as the cumulated size of unique blocks read "
+ "between two consecutive accesses on the same block.");
+DEFINE_string(
+ reuse_distance_buckets, "",
+ "Group blocks by their reuse distances given these buckets. For "
+ "example, if 'reuse_distance_buckets' is '1K,1M,1G', we will "
+ "create four buckets. The first three buckets contain the number of "
+ "blocks with reuse distance less than 1KB, between 1K and 1M, between 1M "
+ "and 1G, respectively. The last bucket contains the number of blocks with "
+ "reuse distance larger than 1G. ");
+DEFINE_string(
+ reuse_interval_labels, "",
+ "Group the reuse interval of a block using these labels. Reuse "
+ "interval is defined as the time between two consecutive accesses "
+ "on the same block.");
+DEFINE_string(
+ reuse_interval_buckets, "",
+ "Group blocks by their reuse interval given these buckets. For "
+ "example, if 'reuse_distance_buckets' is '1,10,100', we will "
+ "create four buckets. The first three buckets contain the number of "
+ "blocks with reuse interval less than 1 second, between 1 second and 10 "
+ "seconds, between 10 seconds and 100 seconds, respectively. The last "
+ "bucket contains the number of blocks with reuse interval longer than 100 "
+ "seconds.");
+DEFINE_string(
+ reuse_lifetime_labels, "",
+ "Group the reuse lifetime of a block using these labels. Reuse "
+ "lifetime is defined as the time interval between the first access on a "
+ "block and the last access on the same block. For blocks that are only "
+ "accessed once, its lifetime is set to kMaxUint64.");
+DEFINE_string(
+ reuse_lifetime_buckets, "",
+ "Group blocks by their reuse lifetime given these buckets. For "
+ "example, if 'reuse_lifetime_buckets' is '1,10,100', we will "
+ "create four buckets. The first three buckets contain the number of "
+ "blocks with reuse lifetime less than 1 second, between 1 second and 10 "
+ "seconds, between 10 seconds and 100 seconds, respectively. The last "
+ "bucket contains the number of blocks with reuse lifetime longer than 100 "
+ "seconds.");
+DEFINE_string(
+ analyze_callers, "",
+ "The list of callers to perform a detailed analysis on. If speicfied, the "
+ "analyzer will output a detailed percentage of accesses for each caller "
+ "break down by column family, level, and block type. A list of available "
+ "callers are: Get, MultiGet, Iterator, ApproximateSize, VerifyChecksum, "
+ "SSTDumpTool, ExternalSSTIngestion, Repair, Prefetch, Compaction, "
+ "CompactionRefill, Flush, SSTFileReader, Uncategorized.");
+DEFINE_string(access_count_buckets, "",
+ "Group number of blocks by their access count given these "
+ "buckets. If specified, the analyzer will output a detailed "
+ "analysis on the number of blocks grouped by their access count "
+ "break down by block type and column family.");
+DEFINE_int32(analyze_blocks_reuse_k_reuse_window, 0,
+ "Analyze the percentage of blocks that are accessed in the "
+ "[k, 2*k] seconds are accessed again in the next [2*k, 3*k], "
+ "[3*k, 4*k],...,[k*(n-1), k*n] seconds. ");
+DEFINE_string(analyze_get_spatial_locality_labels, "",
+ "Group data blocks using these labels.");
+DEFINE_string(analyze_get_spatial_locality_buckets, "",
+ "Group data blocks by their statistics using these buckets.");
+DEFINE_string(skew_labels, "",
+ "Group the access count of a block using these labels.");
+DEFINE_string(skew_buckets, "", "Group the skew labels using these buckets.");
+DEFINE_bool(mrc_only, false,
+ "Evaluate alternative cache policies only. When this flag is true, "
+ "the analyzer does NOT maintain states of each block in memory for "
+ "analysis. It only feeds the accesses into the cache simulators.");
+DEFINE_string(
+ analyze_correlation_coefficients_labels, "",
+ "Analyze the correlation coefficients of features such as number of past "
+ "accesses with regard to the number of accesses till the next access.");
+DEFINE_int32(analyze_correlation_coefficients_max_number_of_values, 1000000,
+ "The maximum number of values for a feature. If the number of "
+ "values for a feature is larger than this max, it randomly "
+ "selects 'max' number of values.");
+DEFINE_string(human_readable_trace_file_path, "",
+ "The filt path that saves human readable access records.");
+
+namespace ROCKSDB_NAMESPACE {
+namespace {
+
+const std::string kMissRatioCurveFileName = "mrc";
+const std::string kGroupbyBlock = "block";
+const std::string kGroupbyTable = "table";
+const std::string kGroupbyColumnFamily = "cf";
+const std::string kGroupbySSTFile = "sst";
+const std::string kGroupbyBlockType = "bt";
+const std::string kGroupbyCaller = "caller";
+const std::string kGroupbyLevel = "level";
+const std::string kGroupbyAll = "all";
+const std::set<std::string> kGroupbyLabels{
+ kGroupbyBlock, kGroupbyColumnFamily, kGroupbySSTFile, kGroupbyLevel,
+ kGroupbyBlockType, kGroupbyCaller, kGroupbyAll};
+const std::string kSupportedCacheNames =
+ " lru ghost_lru lru_priority ghost_lru_priority lru_hybrid "
+ "ghost_lru_hybrid lru_hybrid_no_insert_on_row_miss "
+ "ghost_lru_hybrid_no_insert_on_row_miss ";
+
+// The suffix for the generated csv files.
+const std::string kFileNameSuffixMissRatioTimeline = "miss_ratio_timeline";
+const std::string kFileNameSuffixMissTimeline = "miss_timeline";
+const std::string kFileNameSuffixSkew = "skewness";
+const std::string kFileNameSuffixAccessTimeline = "access_timeline";
+const std::string kFileNameSuffixCorrelation = "correlation_input";
+const std::string kFileNameSuffixAvgReuseIntervalNaccesses =
+ "avg_reuse_interval_naccesses";
+const std::string kFileNameSuffixAvgReuseInterval = "avg_reuse_interval";
+const std::string kFileNameSuffixReuseInterval = "access_reuse_interval";
+const std::string kFileNameSuffixReuseLifetime = "reuse_lifetime";
+const std::string kFileNameSuffixAccessReuseBlocksTimeline =
+ "reuse_blocks_timeline";
+const std::string kFileNameSuffixPercentOfAccessSummary =
+ "percentage_of_accesses_summary";
+const std::string kFileNameSuffixPercentRefKeys = "percent_ref_keys";
+const std::string kFileNameSuffixPercentDataSizeOnRefKeys =
+ "percent_data_size_on_ref_keys";
+const std::string kFileNameSuffixPercentAccessesOnRefKeys =
+ "percent_accesses_on_ref_keys";
+const std::string kFileNameSuffixAccessCountSummary = "access_count_summary";
+
+std::string block_type_to_string(TraceType type) {
+ switch (type) {
+ case kBlockTraceFilterBlock:
+ return "Filter";
+ case kBlockTraceDataBlock:
+ return "Data";
+ case kBlockTraceIndexBlock:
+ return "Index";
+ case kBlockTraceRangeDeletionBlock:
+ return "RangeDeletion";
+ case kBlockTraceUncompressionDictBlock:
+ return "UncompressionDict";
+ default:
+ break;
+ }
+ // This cannot happen.
+ return "InvalidType";
+}
+
+std::string caller_to_string(TableReaderCaller caller) {
+ switch (caller) {
+ case kUserGet:
+ return "Get";
+ case kUserMultiGet:
+ return "MultiGet";
+ case kUserIterator:
+ return "Iterator";
+ case kUserApproximateSize:
+ return "ApproximateSize";
+ case kUserVerifyChecksum:
+ return "VerifyChecksum";
+ case kSSTDumpTool:
+ return "SSTDumpTool";
+ case kExternalSSTIngestion:
+ return "ExternalSSTIngestion";
+ case kRepair:
+ return "Repair";
+ case kPrefetch:
+ return "Prefetch";
+ case kCompaction:
+ return "Compaction";
+ case kCompactionRefill:
+ return "CompactionRefill";
+ case kFlush:
+ return "Flush";
+ case kSSTFileReader:
+ return "SSTFileReader";
+ case kUncategorized:
+ return "Uncategorized";
+ default:
+ break;
+ }
+ // This cannot happen.
+ return "InvalidCaller";
+}
+
+TableReaderCaller string_to_caller(std::string caller_str) {
+ if (caller_str == "Get") {
+ return kUserGet;
+ } else if (caller_str == "MultiGet") {
+ return kUserMultiGet;
+ } else if (caller_str == "Iterator") {
+ return kUserIterator;
+ } else if (caller_str == "ApproximateSize") {
+ return kUserApproximateSize;
+ } else if (caller_str == "VerifyChecksum") {
+ return kUserVerifyChecksum;
+ } else if (caller_str == "SSTDumpTool") {
+ return kSSTDumpTool;
+ } else if (caller_str == "ExternalSSTIngestion") {
+ return kExternalSSTIngestion;
+ } else if (caller_str == "Repair") {
+ return kRepair;
+ } else if (caller_str == "Prefetch") {
+ return kPrefetch;
+ } else if (caller_str == "Compaction") {
+ return kCompaction;
+ } else if (caller_str == "CompactionRefill") {
+ return kCompactionRefill;
+ } else if (caller_str == "Flush") {
+ return kFlush;
+ } else if (caller_str == "SSTFileReader") {
+ return kSSTFileReader;
+ } else if (caller_str == "Uncategorized") {
+ return kUncategorized;
+ }
+ return TableReaderCaller::kMaxBlockCacheLookupCaller;
+}
+
+bool is_user_access(TableReaderCaller caller) {
+ switch (caller) {
+ case kUserGet:
+ case kUserMultiGet:
+ case kUserIterator:
+ case kUserApproximateSize:
+ case kUserVerifyChecksum:
+ return true;
+ default:
+ break;
+ }
+ return false;
+}
+
+const char kBreakLine[] =
+ "***************************************************************\n";
+
+void print_break_lines(uint32_t num_break_lines) {
+ for (uint32_t i = 0; i < num_break_lines; i++) {
+ fprintf(stdout, kBreakLine);
+ }
+}
+
+double percent(uint64_t numerator, uint64_t denomenator) {
+ if (denomenator == 0) {
+ return -1;
+ }
+ return static_cast<double>(numerator * 100.0 / denomenator);
+}
+
+std::map<uint64_t, uint64_t> adjust_time_unit(
+ const std::map<uint64_t, uint64_t>& time_stats, uint64_t time_unit) {
+ if (time_unit == 1) {
+ return time_stats;
+ }
+ std::map<uint64_t, uint64_t> adjusted_time_stats;
+ for (auto const& time : time_stats) {
+ adjusted_time_stats[static_cast<uint64_t>(time.first / time_unit)] +=
+ time.second;
+ }
+ return adjusted_time_stats;
+}
+} // namespace
+
+void BlockCacheTraceAnalyzer::WriteMissRatioCurves() const {
+ if (!cache_simulator_) {
+ return;
+ }
+ if (output_dir_.empty()) {
+ return;
+ }
+ uint64_t trace_duration =
+ trace_end_timestamp_in_seconds_ - trace_start_timestamp_in_seconds_;
+ uint64_t total_accesses = access_sequence_number_;
+ const std::string output_miss_ratio_curve_path =
+ output_dir_ + "/" + std::to_string(trace_duration) + "_" +
+ std::to_string(total_accesses) + "_" + kMissRatioCurveFileName;
+ std::ofstream out(output_miss_ratio_curve_path);
+ if (!out.is_open()) {
+ return;
+ }
+ // Write header.
+ const std::string header =
+ "cache_name,num_shard_bits,ghost_capacity,capacity,miss_ratio,total_"
+ "accesses";
+ out << header << std::endl;
+ for (auto const& config_caches : cache_simulator_->sim_caches()) {
+ const CacheConfiguration& config = config_caches.first;
+ for (uint32_t i = 0; i < config.cache_capacities.size(); i++) {
+ double miss_ratio =
+ config_caches.second[i]->miss_ratio_stats().miss_ratio();
+ // Write the body.
+ out << config.cache_name;
+ out << ",";
+ out << config.num_shard_bits;
+ out << ",";
+ out << config.ghost_cache_capacity;
+ out << ",";
+ out << config.cache_capacities[i];
+ out << ",";
+ out << std::fixed << std::setprecision(4) << miss_ratio;
+ out << ",";
+ out << config_caches.second[i]->miss_ratio_stats().total_accesses();
+ out << std::endl;
+ }
+ }
+ out.close();
+}
+
+void BlockCacheTraceAnalyzer::UpdateFeatureVectors(
+ const std::vector<uint64_t>& access_sequence_number_timeline,
+ const std::vector<uint64_t>& access_timeline, const std::string& label,
+ std::map<std::string, Features>* label_features,
+ std::map<std::string, Predictions>* label_predictions) const {
+ if (access_sequence_number_timeline.empty() || access_timeline.empty()) {
+ return;
+ }
+ assert(access_timeline.size() == access_sequence_number_timeline.size());
+ uint64_t prev_access_sequence_number = access_sequence_number_timeline[0];
+ uint64_t prev_access_timestamp = access_timeline[0];
+ for (uint32_t i = 0; i < access_sequence_number_timeline.size(); i++) {
+ uint64_t num_accesses_since_last_access =
+ access_sequence_number_timeline[i] - prev_access_sequence_number;
+ uint64_t elapsed_time_since_last_access =
+ access_timeline[i] - prev_access_timestamp;
+ prev_access_sequence_number = access_sequence_number_timeline[i];
+ prev_access_timestamp = access_timeline[i];
+ if (i < access_sequence_number_timeline.size() - 1) {
+ (*label_features)[label].num_accesses_since_last_access.push_back(
+ num_accesses_since_last_access);
+ (*label_features)[label].num_past_accesses.push_back(i);
+ (*label_features)[label].elapsed_time_since_last_access.push_back(
+ elapsed_time_since_last_access);
+ }
+ if (i >= 1) {
+ (*label_predictions)[label].num_accesses_till_next_access.push_back(
+ num_accesses_since_last_access);
+ (*label_predictions)[label].elapsed_time_till_next_access.push_back(
+ elapsed_time_since_last_access);
+ }
+ }
+}
+
+void BlockCacheTraceAnalyzer::WriteMissRatioTimeline(uint64_t time_unit) const {
+ if (!cache_simulator_ || output_dir_.empty()) {
+ return;
+ }
+ std::map<uint64_t, std::map<std::string, std::map<uint64_t, double>>>
+ cs_name_timeline;
+ uint64_t start_time = std::numeric_limits<uint64_t>::max();
+ uint64_t end_time = 0;
+ const std::map<uint64_t, uint64_t>& trace_num_misses =
+ adjust_time_unit(miss_ratio_stats_.num_misses_timeline(), time_unit);
+ const std::map<uint64_t, uint64_t>& trace_num_accesses =
+ adjust_time_unit(miss_ratio_stats_.num_accesses_timeline(), time_unit);
+ assert(trace_num_misses.size() == trace_num_accesses.size());
+ for (auto const& num_miss : trace_num_misses) {
+ uint64_t time = num_miss.first;
+ start_time = std::min(start_time, time);
+ end_time = std::max(end_time, time);
+ uint64_t miss = num_miss.second;
+ auto it = trace_num_accesses.find(time);
+ assert(it != trace_num_accesses.end());
+ uint64_t access = it->second;
+ cs_name_timeline[std::numeric_limits<uint64_t>::max()]["trace"][time] =
+ percent(miss, access);
+ }
+ for (auto const& config_caches : cache_simulator_->sim_caches()) {
+ const CacheConfiguration& config = config_caches.first;
+ std::string cache_label = config.cache_name + "-" +
+ std::to_string(config.num_shard_bits) + "-" +
+ std::to_string(config.ghost_cache_capacity);
+ for (uint32_t i = 0; i < config.cache_capacities.size(); i++) {
+ const std::map<uint64_t, uint64_t>& num_misses = adjust_time_unit(
+ config_caches.second[i]->miss_ratio_stats().num_misses_timeline(),
+ time_unit);
+ const std::map<uint64_t, uint64_t>& num_accesses = adjust_time_unit(
+ config_caches.second[i]->miss_ratio_stats().num_accesses_timeline(),
+ time_unit);
+ assert(num_misses.size() == num_accesses.size());
+ for (auto const& num_miss : num_misses) {
+ uint64_t time = num_miss.first;
+ start_time = std::min(start_time, time);
+ end_time = std::max(end_time, time);
+ uint64_t miss = num_miss.second;
+ auto it = num_accesses.find(time);
+ assert(it != num_accesses.end());
+ uint64_t access = it->second;
+ cs_name_timeline[config.cache_capacities[i]][cache_label][time] =
+ percent(miss, access);
+ }
+ }
+ }
+ for (auto const& it : cs_name_timeline) {
+ const std::string output_miss_ratio_timeline_path =
+ output_dir_ + "/" + std::to_string(it.first) + "_" +
+ std::to_string(time_unit) + "_" + kFileNameSuffixMissRatioTimeline;
+ std::ofstream out(output_miss_ratio_timeline_path);
+ if (!out.is_open()) {
+ return;
+ }
+ std::string header("time");
+ for (uint64_t now = start_time; now <= end_time; now++) {
+ header += ",";
+ header += std::to_string(now);
+ }
+ out << header << std::endl;
+ for (auto const& label : it.second) {
+ std::string row(label.first);
+ for (uint64_t now = start_time; now <= end_time; now++) {
+ auto misses = label.second.find(now);
+ row += ",";
+ if (misses != label.second.end()) {
+ row += std::to_string(misses->second);
+ } else {
+ row += "0";
+ }
+ }
+ out << row << std::endl;
+ }
+ out.close();
+ }
+}
+
+void BlockCacheTraceAnalyzer::WriteMissTimeline(uint64_t time_unit) const {
+ if (!cache_simulator_ || output_dir_.empty()) {
+ return;
+ }
+ std::map<uint64_t, std::map<std::string, std::map<uint64_t, uint64_t>>>
+ cs_name_timeline;
+ uint64_t start_time = std::numeric_limits<uint64_t>::max();
+ uint64_t end_time = 0;
+ const std::map<uint64_t, uint64_t>& trace_num_misses =
+ adjust_time_unit(miss_ratio_stats_.num_misses_timeline(), time_unit);
+ for (auto const& num_miss : trace_num_misses) {
+ uint64_t time = num_miss.first;
+ start_time = std::min(start_time, time);
+ end_time = std::max(end_time, time);
+ uint64_t miss = num_miss.second;
+ cs_name_timeline[std::numeric_limits<uint64_t>::max()]["trace"][time] =
+ miss;
+ }
+ for (auto const& config_caches : cache_simulator_->sim_caches()) {
+ const CacheConfiguration& config = config_caches.first;
+ std::string cache_label = config.cache_name + "-" +
+ std::to_string(config.num_shard_bits) + "-" +
+ std::to_string(config.ghost_cache_capacity);
+ for (uint32_t i = 0; i < config.cache_capacities.size(); i++) {
+ const std::map<uint64_t, uint64_t>& num_misses = adjust_time_unit(
+ config_caches.second[i]->miss_ratio_stats().num_misses_timeline(),
+ time_unit);
+ for (auto const& num_miss : num_misses) {
+ uint64_t time = num_miss.first;
+ start_time = std::min(start_time, time);
+ end_time = std::max(end_time, time);
+ uint64_t miss = num_miss.second;
+ cs_name_timeline[config.cache_capacities[i]][cache_label][time] = miss;
+ }
+ }
+ }
+ for (auto const& it : cs_name_timeline) {
+ const std::string output_miss_ratio_timeline_path =
+ output_dir_ + "/" + std::to_string(it.first) + "_" +
+ std::to_string(time_unit) + "_" + kFileNameSuffixMissTimeline;
+ std::ofstream out(output_miss_ratio_timeline_path);
+ if (!out.is_open()) {
+ return;
+ }
+ std::string header("time");
+ for (uint64_t now = start_time; now <= end_time; now++) {
+ header += ",";
+ header += std::to_string(now);
+ }
+ out << header << std::endl;
+ for (auto const& label : it.second) {
+ std::string row(label.first);
+ for (uint64_t now = start_time; now <= end_time; now++) {
+ auto misses = label.second.find(now);
+ row += ",";
+ if (misses != label.second.end()) {
+ row += std::to_string(misses->second);
+ } else {
+ row += "0";
+ }
+ }
+ out << row << std::endl;
+ }
+ out.close();
+ }
+}
+
+void BlockCacheTraceAnalyzer::WriteSkewness(
+ const std::string& label_str, const std::vector<uint64_t>& percent_buckets,
+ TraceType target_block_type) const {
+ std::set<std::string> labels = ParseLabelStr(label_str);
+ std::map<std::string, uint64_t> label_naccesses;
+ uint64_t total_naccesses = 0;
+ auto block_callback = [&](const std::string& cf_name, uint64_t fd,
+ uint32_t level, TraceType type,
+ const std::string& /*block_key*/, uint64_t block_id,
+ const BlockAccessInfo& block) {
+ if (target_block_type != TraceType::kTraceMax &&
+ target_block_type != type) {
+ return;
+ }
+ const std::string label = BuildLabel(
+ labels, cf_name, fd, level, type,
+ TableReaderCaller::kMaxBlockCacheLookupCaller, block_id, block);
+ label_naccesses[label] += block.num_accesses;
+ total_naccesses += block.num_accesses;
+ };
+ TraverseBlocks(block_callback, &labels);
+ std::map<std::string, std::map<uint64_t, uint64_t>> label_bucket_naccesses;
+ std::vector<std::pair<std::string, uint64_t>> pairs;
+ for (auto const& itr : label_naccesses) {
+ pairs.push_back(itr);
+ }
+ // Sort in descending order.
+ sort(pairs.begin(), pairs.end(),
+ [](const std::pair<std::string, uint64_t>& a,
+ const std::pair<std::string, uint64_t>& b) {
+ return b.second < a.second;
+ });
+
+ size_t prev_start_index = 0;
+ for (auto const& percent : percent_buckets) {
+ label_bucket_naccesses[label_str][percent] = 0;
+ size_t end_index = 0;
+ if (percent == std::numeric_limits<uint64_t>::max()) {
+ end_index = label_naccesses.size();
+ } else {
+ end_index = percent * label_naccesses.size() / 100;
+ }
+ for (size_t i = prev_start_index; i < end_index; i++) {
+ label_bucket_naccesses[label_str][percent] += pairs[i].second;
+ }
+ prev_start_index = end_index;
+ }
+ std::string filename_suffix;
+ if (target_block_type != TraceType::kTraceMax) {
+ filename_suffix = block_type_to_string(target_block_type);
+ filename_suffix += "_";
+ }
+ filename_suffix += kFileNameSuffixSkew;
+ WriteStatsToFile(label_str, percent_buckets, filename_suffix,
+ label_bucket_naccesses, total_naccesses);
+}
+
+void BlockCacheTraceAnalyzer::WriteCorrelationFeatures(
+ const std::string& label_str, uint32_t max_number_of_values) const {
+ std::set<std::string> labels = ParseLabelStr(label_str);
+ std::map<std::string, Features> label_features;
+ std::map<std::string, Predictions> label_predictions;
+ auto block_callback =
+ [&](const std::string& cf_name, uint64_t fd, uint32_t level,
+ TraceType block_type, const std::string& /*block_key*/,
+ uint64_t /*block_key_id*/, const BlockAccessInfo& block) {
+ if (block.table_id == 0 && labels.find(kGroupbyTable) != labels.end()) {
+ // We only know table id information for get requests.
+ return;
+ }
+ if (labels.find(kGroupbyCaller) != labels.end()) {
+ // Group by caller.
+ for (auto const& caller_map : block.caller_access_timeline) {
+ const std::string label =
+ BuildLabel(labels, cf_name, fd, level, block_type,
+ caller_map.first, /*block_id=*/0, block);
+ auto it = block.caller_access_sequence__number_timeline.find(
+ caller_map.first);
+ assert(it != block.caller_access_sequence__number_timeline.end());
+ UpdateFeatureVectors(it->second, caller_map.second, label,
+ &label_features, &label_predictions);
+ }
+ return;
+ }
+ const std::string label =
+ BuildLabel(labels, cf_name, fd, level, block_type,
+ TableReaderCaller::kMaxBlockCacheLookupCaller,
+ /*block_id=*/0, block);
+ UpdateFeatureVectors(block.access_sequence_number_timeline,
+ block.access_timeline, label, &label_features,
+ &label_predictions);
+ };
+ TraverseBlocks(block_callback, &labels);
+ WriteCorrelationFeaturesToFile(label_str, label_features, label_predictions,
+ max_number_of_values);
+}
+
+void BlockCacheTraceAnalyzer::WriteCorrelationFeaturesToFile(
+ const std::string& label,
+ const std::map<std::string, Features>& label_features,
+ const std::map<std::string, Predictions>& label_predictions,
+ uint32_t max_number_of_values) const {
+ for (auto const& label_feature_vectors : label_features) {
+ const Features& past = label_feature_vectors.second;
+ auto it = label_predictions.find(label_feature_vectors.first);
+ assert(it != label_predictions.end());
+ const Predictions& future = it->second;
+ const std::string output_path = output_dir_ + "/" + label + "_" +
+ label_feature_vectors.first + "_" +
+ kFileNameSuffixCorrelation;
+ std::ofstream out(output_path);
+ if (!out.is_open()) {
+ return;
+ }
+ std::string header(
+ "num_accesses_since_last_access,elapsed_time_since_last_access,num_"
+ "past_accesses,num_accesses_till_next_access,elapsed_time_till_next_"
+ "access");
+ out << header << std::endl;
+ std::vector<uint32_t> indexes;
+ for (uint32_t i = 0; i < past.num_accesses_since_last_access.size(); i++) {
+ indexes.push_back(i);
+ }
+ RandomShuffle(indexes.begin(), indexes.end());
+ for (uint32_t i = 0; i < max_number_of_values && i < indexes.size(); i++) {
+ uint32_t rand_index = indexes[i];
+ out << std::to_string(past.num_accesses_since_last_access[rand_index])
+ << ",";
+ out << std::to_string(past.elapsed_time_since_last_access[rand_index])
+ << ",";
+ out << std::to_string(past.num_past_accesses[rand_index]) << ",";
+ out << std::to_string(future.num_accesses_till_next_access[rand_index])
+ << ",";
+ out << std::to_string(future.elapsed_time_till_next_access[rand_index])
+ << std::endl;
+ }
+ out.close();
+ }
+}
+
+void BlockCacheTraceAnalyzer::WriteCorrelationFeaturesForGet(
+ uint32_t max_number_of_values) const {
+ std::string label = "GetKeyInfo";
+ std::map<std::string, Features> label_features;
+ std::map<std::string, Predictions> label_predictions;
+ for (auto const& get_info : get_key_info_map_) {
+ const GetKeyInfo& info = get_info.second;
+ UpdateFeatureVectors(info.access_sequence_number_timeline,
+ info.access_timeline, label, &label_features,
+ &label_predictions);
+ }
+ WriteCorrelationFeaturesToFile(label, label_features, label_predictions,
+ max_number_of_values);
+}
+
+std::set<std::string> BlockCacheTraceAnalyzer::ParseLabelStr(
+ const std::string& label_str) const {
+ std::stringstream ss(label_str);
+ std::set<std::string> labels;
+ // label_str is in the form of "label1_label2_label3", e.g., cf_bt.
+ while (ss.good()) {
+ std::string label_name;
+ getline(ss, label_name, '_');
+ if (kGroupbyLabels.find(label_name) == kGroupbyLabels.end()) {
+ // Unknown label name.
+ fprintf(stderr, "Unknown label name %s, label string %s\n",
+ label_name.c_str(), label_str.c_str());
+ return {};
+ }
+ labels.insert(label_name);
+ }
+ return labels;
+}
+
+std::string BlockCacheTraceAnalyzer::BuildLabel(
+ const std::set<std::string>& labels, const std::string& cf_name,
+ uint64_t fd, uint32_t level, TraceType type, TableReaderCaller caller,
+ uint64_t block_key, const BlockAccessInfo& block) const {
+ std::map<std::string, std::string> label_value_map;
+ label_value_map[kGroupbyAll] = kGroupbyAll;
+ label_value_map[kGroupbyLevel] = std::to_string(level);
+ label_value_map[kGroupbyCaller] = caller_to_string(caller);
+ label_value_map[kGroupbySSTFile] = std::to_string(fd);
+ label_value_map[kGroupbyBlockType] = block_type_to_string(type);
+ label_value_map[kGroupbyColumnFamily] = cf_name;
+ label_value_map[kGroupbyBlock] = std::to_string(block_key);
+ label_value_map[kGroupbyTable] = std::to_string(block.table_id);
+ // Concatenate the label values.
+ std::string label;
+ for (auto const& l : labels) {
+ label += label_value_map[l];
+ label += "-";
+ }
+ if (!label.empty()) {
+ label.pop_back();
+ }
+ return label;
+}
+
+void BlockCacheTraceAnalyzer::TraverseBlocks(
+ std::function<void(const std::string& /*cf_name*/, uint64_t /*fd*/,
+ uint32_t /*level*/, TraceType /*block_type*/,
+ const std::string& /*block_key*/,
+ uint64_t /*block_key_id*/,
+ const BlockAccessInfo& /*block_access_info*/)>
+ block_callback,
+ std::set<std::string>* labels) const {
+ for (auto const& cf_aggregates : cf_aggregates_map_) {
+ // Stats per column family.
+ const std::string& cf_name = cf_aggregates.first;
+ for (auto const& file_aggregates : cf_aggregates.second.fd_aggregates_map) {
+ // Stats per SST file.
+ const uint64_t fd = file_aggregates.first;
+ const uint32_t level = file_aggregates.second.level;
+ for (auto const& block_type_aggregates :
+ file_aggregates.second.block_type_aggregates_map) {
+ // Stats per block type.
+ const TraceType type = block_type_aggregates.first;
+ for (auto const& block_access_info :
+ block_type_aggregates.second.block_access_info_map) {
+ // Stats per block.
+ if (labels && block_access_info.second.table_id == 0 &&
+ labels->find(kGroupbyTable) != labels->end()) {
+ // We only know table id information for get requests.
+ return;
+ }
+ block_callback(cf_name, fd, level, type, block_access_info.first,
+ block_access_info.second.block_id,
+ block_access_info.second);
+ }
+ }
+ }
+ }
+}
+
+void BlockCacheTraceAnalyzer::WriteGetSpatialLocality(
+ const std::string& label_str,
+ const std::vector<uint64_t>& percent_buckets) const {
+ std::set<std::string> labels = ParseLabelStr(label_str);
+ std::map<std::string, std::map<uint64_t, uint64_t>> label_pnrefkeys_nblocks;
+ std::map<std::string, std::map<uint64_t, uint64_t>> label_pnrefs_nblocks;
+ std::map<std::string, std::map<uint64_t, uint64_t>> label_pndatasize_nblocks;
+ uint64_t nblocks = 0;
+ auto block_callback = [&](const std::string& cf_name, uint64_t fd,
+ uint32_t level, TraceType /*block_type*/,
+ const std::string& /*block_key*/,
+ uint64_t /*block_key_id*/,
+ const BlockAccessInfo& block) {
+ if (block.num_keys == 0) {
+ return;
+ }
+ uint64_t naccesses = 0;
+ for (auto const& key_access : block.key_num_access_map) {
+ for (auto const& caller_access : key_access.second) {
+ if (caller_access.first == TableReaderCaller::kUserGet) {
+ naccesses += caller_access.second;
+ }
+ }
+ }
+ const std::string label =
+ BuildLabel(labels, cf_name, fd, level, TraceType::kBlockTraceDataBlock,
+ TableReaderCaller::kUserGet, /*block_id=*/0, block);
+
+ const uint64_t percent_referenced_for_existing_keys =
+ static_cast<uint64_t>(std::max(
+ percent(block.key_num_access_map.size(), block.num_keys), 0.0));
+ const uint64_t percent_accesses_for_existing_keys =
+ static_cast<uint64_t>(std::max(
+ percent(block.num_referenced_key_exist_in_block, naccesses), 0.0));
+ const uint64_t percent_referenced_data_size = static_cast<uint64_t>(
+ std::max(percent(block.referenced_data_size, block.block_size), 0.0));
+ if (label_pnrefkeys_nblocks.find(label) == label_pnrefkeys_nblocks.end()) {
+ for (auto const& percent_bucket : percent_buckets) {
+ label_pnrefkeys_nblocks[label][percent_bucket] = 0;
+ label_pnrefs_nblocks[label][percent_bucket] = 0;
+ label_pndatasize_nblocks[label][percent_bucket] = 0;
+ }
+ }
+ label_pnrefkeys_nblocks[label]
+ .upper_bound(percent_referenced_for_existing_keys)
+ ->second += 1;
+ label_pnrefs_nblocks[label]
+ .upper_bound(percent_accesses_for_existing_keys)
+ ->second += 1;
+ label_pndatasize_nblocks[label]
+ .upper_bound(percent_referenced_data_size)
+ ->second += 1;
+ nblocks += 1;
+ };
+ TraverseBlocks(block_callback, &labels);
+ WriteStatsToFile(label_str, percent_buckets, kFileNameSuffixPercentRefKeys,
+ label_pnrefkeys_nblocks, nblocks);
+ WriteStatsToFile(label_str, percent_buckets,
+ kFileNameSuffixPercentAccessesOnRefKeys,
+ label_pnrefs_nblocks, nblocks);
+ WriteStatsToFile(label_str, percent_buckets,
+ kFileNameSuffixPercentDataSizeOnRefKeys,
+ label_pndatasize_nblocks, nblocks);
+}
+
+void BlockCacheTraceAnalyzer::WriteAccessTimeline(const std::string& label_str,
+ uint64_t time_unit,
+ bool user_access_only) const {
+ std::set<std::string> labels = ParseLabelStr(label_str);
+ uint64_t start_time = std::numeric_limits<uint64_t>::max();
+ uint64_t end_time = 0;
+ std::map<std::string, std::map<uint64_t, uint64_t>> label_access_timeline;
+ std::map<uint64_t, std::vector<std::string>> access_count_block_id_map;
+
+ auto block_callback = [&](const std::string& cf_name, uint64_t fd,
+ uint32_t level, TraceType type,
+ const std::string& /*block_key*/, uint64_t block_id,
+ const BlockAccessInfo& block) {
+ uint64_t naccesses = 0;
+ for (auto const& timeline : block.caller_num_accesses_timeline) {
+ const TableReaderCaller caller = timeline.first;
+ if (user_access_only && !is_user_access(caller)) {
+ continue;
+ }
+ const std::string label =
+ BuildLabel(labels, cf_name, fd, level, type, caller, block_id, block);
+ for (auto const& naccess : timeline.second) {
+ const uint64_t timestamp = naccess.first / time_unit;
+ const uint64_t num = naccess.second;
+ label_access_timeline[label][timestamp] += num;
+ start_time = std::min(start_time, timestamp);
+ end_time = std::max(end_time, timestamp);
+ naccesses += num;
+ }
+ }
+ if (naccesses > 0) {
+ access_count_block_id_map[naccesses].push_back(std::to_string(block_id));
+ }
+ };
+ TraverseBlocks(block_callback, &labels);
+
+ // We have label_access_timeline now. Write them into a file.
+ const std::string user_access_prefix =
+ user_access_only ? "user_access_only_" : "all_access_";
+ const std::string output_path = output_dir_ + "/" + user_access_prefix +
+ label_str + "_" + std::to_string(time_unit) +
+ "_" + kFileNameSuffixAccessTimeline;
+ std::ofstream out(output_path);
+ if (!out.is_open()) {
+ return;
+ }
+ std::string header("time");
+ if (labels.find("block") != labels.end()) {
+ for (uint64_t now = start_time; now <= end_time; now++) {
+ header += ",";
+ header += std::to_string(now);
+ }
+ out << header << std::endl;
+ // Write the most frequently accessed blocks first.
+ for (auto naccess_it = access_count_block_id_map.rbegin();
+ naccess_it != access_count_block_id_map.rend(); naccess_it++) {
+ for (auto& block_id_it : naccess_it->second) {
+ std::string row(block_id_it);
+ for (uint64_t now = start_time; now <= end_time; now++) {
+ auto it = label_access_timeline[block_id_it].find(now);
+ row += ",";
+ if (it != label_access_timeline[block_id_it].end()) {
+ row += std::to_string(it->second);
+ } else {
+ row += "0";
+ }
+ }
+ out << row << std::endl;
+ }
+ }
+ out.close();
+ return;
+ }
+ for (uint64_t now = start_time; now <= end_time; now++) {
+ header += ",";
+ header += std::to_string(now);
+ }
+ out << header << std::endl;
+ for (auto const& label : label_access_timeline) {
+ std::string row(label.first);
+ for (uint64_t now = start_time; now <= end_time; now++) {
+ auto it = label.second.find(now);
+ row += ",";
+ if (it != label.second.end()) {
+ row += std::to_string(it->second);
+ } else {
+ row += "0";
+ }
+ }
+ out << row << std::endl;
+ }
+
+ out.close();
+}
+
+void BlockCacheTraceAnalyzer::WriteReuseDistance(
+ const std::string& label_str,
+ const std::vector<uint64_t>& distance_buckets) const {
+ std::set<std::string> labels = ParseLabelStr(label_str);
+ std::map<std::string, std::map<uint64_t, uint64_t>> label_distance_num_reuses;
+ uint64_t total_num_reuses = 0;
+ auto block_callback = [&](const std::string& cf_name, uint64_t fd,
+ uint32_t level, TraceType type,
+ const std::string& /*block_key*/, uint64_t block_id,
+ const BlockAccessInfo& block) {
+ const std::string label = BuildLabel(
+ labels, cf_name, fd, level, type,
+ TableReaderCaller::kMaxBlockCacheLookupCaller, block_id, block);
+ if (label_distance_num_reuses.find(label) ==
+ label_distance_num_reuses.end()) {
+ // The first time we encounter this label.
+ for (auto const& distance_bucket : distance_buckets) {
+ label_distance_num_reuses[label][distance_bucket] = 0;
+ }
+ }
+ for (auto const& reuse_distance : block.reuse_distance_count) {
+ label_distance_num_reuses[label]
+ .upper_bound(reuse_distance.first)
+ ->second += reuse_distance.second;
+ total_num_reuses += reuse_distance.second;
+ }
+ };
+ TraverseBlocks(block_callback, &labels);
+ // We have label_naccesses and label_distance_num_reuses now. Write them into
+ // a file.
+ const std::string output_path =
+ output_dir_ + "/" + label_str + "_reuse_distance";
+ std::ofstream out(output_path);
+ if (!out.is_open()) {
+ return;
+ }
+ std::string header("bucket");
+ for (auto const& label_it : label_distance_num_reuses) {
+ header += ",";
+ header += label_it.first;
+ }
+ out << header << std::endl;
+ for (auto const& bucket : distance_buckets) {
+ std::string row(std::to_string(bucket));
+ for (auto const& label_it : label_distance_num_reuses) {
+ auto const& it = label_it.second.find(bucket);
+ assert(it != label_it.second.end());
+ row += ",";
+ row += std::to_string(percent(it->second, total_num_reuses));
+ }
+ out << row << std::endl;
+ }
+ out.close();
+}
+
+void BlockCacheTraceAnalyzer::UpdateReuseIntervalStats(
+ const std::string& label, const std::vector<uint64_t>& time_buckets,
+ const std::map<uint64_t, uint64_t> timeline,
+ std::map<std::string, std::map<uint64_t, uint64_t>>* label_time_num_reuses,
+ uint64_t* total_num_reuses) const {
+ assert(label_time_num_reuses);
+ assert(total_num_reuses);
+ if (label_time_num_reuses->find(label) == label_time_num_reuses->end()) {
+ // The first time we encounter this label.
+ for (auto const& time_bucket : time_buckets) {
+ (*label_time_num_reuses)[label][time_bucket] = 0;
+ }
+ }
+ auto it = timeline.begin();
+ uint64_t prev_timestamp = it->first;
+ const uint64_t prev_num = it->second;
+ it++;
+ // Reused within one second.
+ if (prev_num > 1) {
+ (*label_time_num_reuses)[label].upper_bound(0)->second += prev_num - 1;
+ *total_num_reuses += prev_num - 1;
+ }
+ while (it != timeline.end()) {
+ const uint64_t timestamp = it->first;
+ const uint64_t num = it->second;
+ const uint64_t reuse_interval = timestamp - prev_timestamp;
+ (*label_time_num_reuses)[label].upper_bound(reuse_interval)->second += 1;
+ if (num > 1) {
+ (*label_time_num_reuses)[label].upper_bound(0)->second += num - 1;
+ }
+ prev_timestamp = timestamp;
+ *total_num_reuses += num;
+ it++;
+ }
+}
+
+void BlockCacheTraceAnalyzer::WriteStatsToFile(
+ const std::string& label_str, const std::vector<uint64_t>& time_buckets,
+ const std::string& filename_suffix,
+ const std::map<std::string, std::map<uint64_t, uint64_t>>& label_data,
+ uint64_t ntotal) const {
+ const std::string output_path =
+ output_dir_ + "/" + label_str + "_" + filename_suffix;
+ std::ofstream out(output_path);
+ if (!out.is_open()) {
+ return;
+ }
+ std::string header("bucket");
+ for (auto const& label_it : label_data) {
+ header += ",";
+ header += label_it.first;
+ }
+ out << header << std::endl;
+ for (auto const& bucket : time_buckets) {
+ std::string row(std::to_string(bucket));
+ for (auto const& label_it : label_data) {
+ auto const& it = label_it.second.find(bucket);
+ assert(it != label_it.second.end());
+ row += ",";
+ row += std::to_string(percent(it->second, ntotal));
+ }
+ out << row << std::endl;
+ }
+ out.close();
+}
+
+void BlockCacheTraceAnalyzer::WriteReuseInterval(
+ const std::string& label_str,
+ const std::vector<uint64_t>& time_buckets) const {
+ std::set<std::string> labels = ParseLabelStr(label_str);
+ std::map<std::string, std::map<uint64_t, uint64_t>> label_time_num_reuses;
+ std::map<std::string, std::map<uint64_t, uint64_t>> label_avg_reuse_nblocks;
+ std::map<std::string, std::map<uint64_t, uint64_t>> label_avg_reuse_naccesses;
+
+ uint64_t total_num_reuses = 0;
+ uint64_t total_nblocks = 0;
+ uint64_t total_accesses = 0;
+ auto block_callback = [&](const std::string& cf_name, uint64_t fd,
+ uint32_t level, TraceType type,
+ const std::string& /*block_key*/, uint64_t block_id,
+ const BlockAccessInfo& block) {
+ total_nblocks++;
+ total_accesses += block.num_accesses;
+ uint64_t avg_reuse_interval = 0;
+ if (block.num_accesses > 1) {
+ avg_reuse_interval = ((block.last_access_time - block.first_access_time) /
+ kMicrosInSecond) /
+ block.num_accesses;
+ } else {
+ avg_reuse_interval = std::numeric_limits<uint64_t>::max() - 1;
+ }
+ if (labels.find(kGroupbyCaller) != labels.end()) {
+ for (auto const& timeline : block.caller_num_accesses_timeline) {
+ const TableReaderCaller caller = timeline.first;
+ const std::string label = BuildLabel(labels, cf_name, fd, level, type,
+ caller, block_id, block);
+ UpdateReuseIntervalStats(label, time_buckets, timeline.second,
+ &label_time_num_reuses, &total_num_reuses);
+ }
+ return;
+ }
+ // Does not group by caller so we need to flatten the access timeline.
+ const std::string label = BuildLabel(
+ labels, cf_name, fd, level, type,
+ TableReaderCaller::kMaxBlockCacheLookupCaller, block_id, block);
+ std::map<uint64_t, uint64_t> timeline;
+ for (auto const& caller_timeline : block.caller_num_accesses_timeline) {
+ for (auto const& time_naccess : caller_timeline.second) {
+ timeline[time_naccess.first] += time_naccess.second;
+ }
+ }
+ UpdateReuseIntervalStats(label, time_buckets, timeline,
+ &label_time_num_reuses, &total_num_reuses);
+ if (label_avg_reuse_nblocks.find(label) == label_avg_reuse_nblocks.end()) {
+ for (auto const& time_bucket : time_buckets) {
+ label_avg_reuse_nblocks[label][time_bucket] = 0;
+ label_avg_reuse_naccesses[label][time_bucket] = 0;
+ }
+ }
+ label_avg_reuse_nblocks[label].upper_bound(avg_reuse_interval)->second += 1;
+ label_avg_reuse_naccesses[label].upper_bound(avg_reuse_interval)->second +=
+ block.num_accesses;
+ };
+ TraverseBlocks(block_callback, &labels);
+
+ // Write the stats into files.
+ WriteStatsToFile(label_str, time_buckets, kFileNameSuffixReuseInterval,
+ label_time_num_reuses, total_num_reuses);
+ WriteStatsToFile(label_str, time_buckets, kFileNameSuffixAvgReuseInterval,
+ label_avg_reuse_nblocks, total_nblocks);
+ WriteStatsToFile(label_str, time_buckets,
+ kFileNameSuffixAvgReuseIntervalNaccesses,
+ label_avg_reuse_naccesses, total_accesses);
+}
+
+void BlockCacheTraceAnalyzer::WriteReuseLifetime(
+ const std::string& label_str,
+ const std::vector<uint64_t>& time_buckets) const {
+ std::set<std::string> labels = ParseLabelStr(label_str);
+ std::map<std::string, std::map<uint64_t, uint64_t>> label_lifetime_nblocks;
+ uint64_t total_nblocks = 0;
+ auto block_callback = [&](const std::string& cf_name, uint64_t fd,
+ uint32_t level, TraceType type,
+ const std::string& /*block_key*/, uint64_t block_id,
+ const BlockAccessInfo& block) {
+ uint64_t lifetime = 0;
+ if (block.num_accesses > 1) {
+ lifetime =
+ (block.last_access_time - block.first_access_time) / kMicrosInSecond;
+ } else {
+ lifetime = std::numeric_limits<uint64_t>::max() - 1;
+ }
+ const std::string label = BuildLabel(
+ labels, cf_name, fd, level, type,
+ TableReaderCaller::kMaxBlockCacheLookupCaller, block_id, block);
+
+ if (label_lifetime_nblocks.find(label) == label_lifetime_nblocks.end()) {
+ // The first time we encounter this label.
+ for (auto const& time_bucket : time_buckets) {
+ label_lifetime_nblocks[label][time_bucket] = 0;
+ }
+ }
+ label_lifetime_nblocks[label].upper_bound(lifetime)->second += 1;
+ total_nblocks += 1;
+ };
+ TraverseBlocks(block_callback, &labels);
+ WriteStatsToFile(label_str, time_buckets, kFileNameSuffixReuseLifetime,
+ label_lifetime_nblocks, total_nblocks);
+}
+
+void BlockCacheTraceAnalyzer::WriteBlockReuseTimeline(
+ const uint64_t reuse_window, bool user_access_only,
+ TraceType block_type) const {
+ // A map from block key to an array of bools that states whether a block is
+ // accessed in a time window.
+ std::map<uint64_t, std::vector<bool>> block_accessed;
+ const uint64_t trace_duration =
+ trace_end_timestamp_in_seconds_ - trace_start_timestamp_in_seconds_;
+ const uint64_t reuse_vector_size = (trace_duration / reuse_window);
+ if (reuse_vector_size < 2) {
+ // The reuse window is less than 2. We cannot calculate the reused
+ // percentage of blocks.
+ return;
+ }
+ auto block_callback = [&](const std::string& /*cf_name*/, uint64_t /*fd*/,
+ uint32_t /*level*/, TraceType /*type*/,
+ const std::string& /*block_key*/, uint64_t block_id,
+ const BlockAccessInfo& block) {
+ if (block_accessed.find(block_id) == block_accessed.end()) {
+ block_accessed[block_id].resize(reuse_vector_size);
+ for (uint64_t i = 0; i < reuse_vector_size; i++) {
+ block_accessed[block_id][i] = false;
+ }
+ }
+ for (auto const& caller_num : block.caller_num_accesses_timeline) {
+ const TableReaderCaller caller = caller_num.first;
+ for (auto const& timeline : caller_num.second) {
+ const uint64_t timestamp = timeline.first;
+ const uint64_t elapsed_time =
+ timestamp - trace_start_timestamp_in_seconds_;
+ if (!user_access_only || is_user_access(caller)) {
+ uint64_t index =
+ std::min(elapsed_time / reuse_window, reuse_vector_size - 1);
+ block_accessed[block_id][index] = true;
+ }
+ }
+ }
+ };
+ TraverseBlocks(block_callback);
+
+ // A cell is the number of blocks accessed in a reuse window.
+ std::unique_ptr<uint64_t[]> reuse_table(
+ new uint64_t[reuse_vector_size * reuse_vector_size]);
+ for (uint64_t start_time = 0; start_time < reuse_vector_size; start_time++) {
+ // Initialize the reuse_table.
+ for (uint64_t i = 0; i < reuse_vector_size; i++) {
+ reuse_table[start_time * reuse_vector_size + i] = 0;
+ }
+ // Examine all blocks.
+ for (auto const& block : block_accessed) {
+ for (uint64_t i = start_time; i < reuse_vector_size; i++) {
+ if (block.second[start_time] && block.second[i]) {
+ // This block is accessed at start time and at the current time. We
+ // increment reuse_table[start_time][i] since it is reused at the ith
+ // window.
+ reuse_table[start_time * reuse_vector_size + i]++;
+ }
+ }
+ }
+ }
+ const std::string user_access_prefix =
+ user_access_only ? "_user_access_only_" : "_all_access_";
+ const std::string output_path =
+ output_dir_ + "/" + block_type_to_string(block_type) +
+ user_access_prefix + std::to_string(reuse_window) + "_" +
+ kFileNameSuffixAccessReuseBlocksTimeline;
+ std::ofstream out(output_path);
+ if (!out.is_open()) {
+ return;
+ }
+ std::string header("start_time");
+ for (uint64_t start_time = 0; start_time < reuse_vector_size; start_time++) {
+ header += ",";
+ header += std::to_string(start_time);
+ }
+ out << header << std::endl;
+ for (uint64_t start_time = 0; start_time < reuse_vector_size; start_time++) {
+ std::string row(std::to_string(start_time * reuse_window));
+ for (uint64_t j = 0; j < reuse_vector_size; j++) {
+ row += ",";
+ if (j < start_time) {
+ row += "100.0";
+ } else {
+ row += std::to_string(
+ percent(reuse_table[start_time * reuse_vector_size + j],
+ reuse_table[start_time * reuse_vector_size + start_time]));
+ }
+ }
+ out << row << std::endl;
+ }
+ out.close();
+}
+
+std::string BlockCacheTraceAnalyzer::OutputPercentAccessStats(
+ uint64_t total_accesses,
+ const std::map<std::string, uint64_t>& cf_access_count) const {
+ std::string row;
+ for (auto const& cf_aggregates : cf_aggregates_map_) {
+ const std::string& cf_name = cf_aggregates.first;
+ const auto& naccess = cf_access_count.find(cf_name);
+ row += ",";
+ if (naccess != cf_access_count.end()) {
+ row += std::to_string(percent(naccess->second, total_accesses));
+ } else {
+ row += "0";
+ }
+ }
+ return row;
+}
+
+void BlockCacheTraceAnalyzer::WritePercentAccessSummaryStats() const {
+ std::map<TableReaderCaller, std::map<std::string, uint64_t>>
+ caller_cf_accesses;
+ uint64_t total_accesses = 0;
+ auto block_callback =
+ [&](const std::string& cf_name, uint64_t /*fd*/, uint32_t /*level*/,
+ TraceType /*type*/, const std::string& /*block_key*/,
+ uint64_t /*block_id*/, const BlockAccessInfo& block) {
+ for (auto const& caller_num : block.caller_num_access_map) {
+ const TableReaderCaller caller = caller_num.first;
+ const uint64_t naccess = caller_num.second;
+ caller_cf_accesses[caller][cf_name] += naccess;
+ total_accesses += naccess;
+ }
+ };
+ TraverseBlocks(block_callback);
+
+ const std::string output_path =
+ output_dir_ + "/" + kFileNameSuffixPercentOfAccessSummary;
+ std::ofstream out(output_path);
+ if (!out.is_open()) {
+ return;
+ }
+ std::string header("caller");
+ for (auto const& cf_name : cf_aggregates_map_) {
+ header += ",";
+ header += cf_name.first;
+ }
+ out << header << std::endl;
+ for (auto const& cf_naccess_it : caller_cf_accesses) {
+ const TableReaderCaller caller = cf_naccess_it.first;
+ std::string row;
+ row += caller_to_string(caller);
+ row += OutputPercentAccessStats(total_accesses, cf_naccess_it.second);
+ out << row << std::endl;
+ }
+ out.close();
+}
+
+void BlockCacheTraceAnalyzer::WriteDetailedPercentAccessSummaryStats(
+ TableReaderCaller analyzing_caller) const {
+ std::map<uint32_t, std::map<std::string, uint64_t>> level_cf_accesses;
+ std::map<TraceType, std::map<std::string, uint64_t>> bt_cf_accesses;
+ uint64_t total_accesses = 0;
+ auto block_callback =
+ [&](const std::string& cf_name, uint64_t /*fd*/, uint32_t level,
+ TraceType type, const std::string& /*block_key*/,
+ uint64_t /*block_id*/, const BlockAccessInfo& block) {
+ for (auto const& caller_num : block.caller_num_access_map) {
+ const TableReaderCaller caller = caller_num.first;
+ if (caller == analyzing_caller) {
+ const uint64_t naccess = caller_num.second;
+ level_cf_accesses[level][cf_name] += naccess;
+ bt_cf_accesses[type][cf_name] += naccess;
+ total_accesses += naccess;
+ }
+ }
+ };
+ TraverseBlocks(block_callback);
+ {
+ const std::string output_path =
+ output_dir_ + "/" + caller_to_string(analyzing_caller) + "_level_" +
+ kFileNameSuffixPercentOfAccessSummary;
+ std::ofstream out(output_path);
+ if (!out.is_open()) {
+ return;
+ }
+ std::string header("level");
+ for (auto const& cf_name : cf_aggregates_map_) {
+ header += ",";
+ header += cf_name.first;
+ }
+ out << header << std::endl;
+ for (auto const& level_naccess_it : level_cf_accesses) {
+ const uint32_t level = level_naccess_it.first;
+ std::string row;
+ row += std::to_string(level);
+ row += OutputPercentAccessStats(total_accesses, level_naccess_it.second);
+ out << row << std::endl;
+ }
+ out.close();
+ }
+ {
+ const std::string output_path =
+ output_dir_ + "/" + caller_to_string(analyzing_caller) + "_bt_" +
+ kFileNameSuffixPercentOfAccessSummary;
+ std::ofstream out(output_path);
+ if (!out.is_open()) {
+ return;
+ }
+ std::string header("bt");
+ for (auto const& cf_name : cf_aggregates_map_) {
+ header += ",";
+ header += cf_name.first;
+ }
+ out << header << std::endl;
+ for (auto const& bt_naccess_it : bt_cf_accesses) {
+ const TraceType bt = bt_naccess_it.first;
+ std::string row;
+ row += block_type_to_string(bt);
+ row += OutputPercentAccessStats(total_accesses, bt_naccess_it.second);
+ out << row << std::endl;
+ }
+ out.close();
+ }
+}
+
+void BlockCacheTraceAnalyzer::WriteAccessCountSummaryStats(
+ const std::vector<uint64_t>& access_count_buckets,
+ bool user_access_only) const {
+ // x: buckets.
+ // y: # of accesses.
+ std::map<std::string, std::map<uint64_t, uint64_t>> bt_access_nblocks;
+ std::map<std::string, std::map<uint64_t, uint64_t>> cf_access_nblocks;
+ uint64_t total_nblocks = 0;
+ auto block_callback =
+ [&](const std::string& cf_name, uint64_t /*fd*/, uint32_t /*level*/,
+ TraceType type, const std::string& /*block_key*/,
+ uint64_t /*block_id*/, const BlockAccessInfo& block) {
+ const std::string type_str = block_type_to_string(type);
+ if (cf_access_nblocks.find(cf_name) == cf_access_nblocks.end()) {
+ // initialize.
+ for (auto& access : access_count_buckets) {
+ cf_access_nblocks[cf_name][access] = 0;
+ }
+ }
+ if (bt_access_nblocks.find(type_str) == bt_access_nblocks.end()) {
+ // initialize.
+ for (auto& access : access_count_buckets) {
+ bt_access_nblocks[type_str][access] = 0;
+ }
+ }
+ uint64_t naccesses = 0;
+ for (auto const& caller_access : block.caller_num_access_map) {
+ if (!user_access_only || is_user_access(caller_access.first)) {
+ naccesses += caller_access.second;
+ }
+ }
+ if (naccesses == 0) {
+ return;
+ }
+ total_nblocks += 1;
+ bt_access_nblocks[type_str].upper_bound(naccesses)->second += 1;
+ cf_access_nblocks[cf_name].upper_bound(naccesses)->second += 1;
+ };
+ TraverseBlocks(block_callback);
+ const std::string user_access_prefix =
+ user_access_only ? "user_access_only_" : "all_access_";
+ WriteStatsToFile("cf", access_count_buckets,
+ user_access_prefix + kFileNameSuffixAccessCountSummary,
+ cf_access_nblocks, total_nblocks);
+ WriteStatsToFile("bt", access_count_buckets,
+ user_access_prefix + kFileNameSuffixAccessCountSummary,
+ bt_access_nblocks, total_nblocks);
+}
+
+BlockCacheTraceAnalyzer::BlockCacheTraceAnalyzer(
+ const std::string& trace_file_path, const std::string& output_dir,
+ const std::string& human_readable_trace_file_path,
+ bool compute_reuse_distance, bool mrc_only,
+ bool is_human_readable_trace_file,
+ std::unique_ptr<BlockCacheTraceSimulator>&& cache_simulator)
+ : env_(ROCKSDB_NAMESPACE::Env::Default()),
+ trace_file_path_(trace_file_path),
+ output_dir_(output_dir),
+ human_readable_trace_file_path_(human_readable_trace_file_path),
+ compute_reuse_distance_(compute_reuse_distance),
+ mrc_only_(mrc_only),
+ is_human_readable_trace_file_(is_human_readable_trace_file),
+ cache_simulator_(std::move(cache_simulator)) {}
+
+void BlockCacheTraceAnalyzer::ComputeReuseDistance(
+ BlockAccessInfo* info) const {
+ assert(info);
+ if (info->num_accesses == 0) {
+ return;
+ }
+ uint64_t reuse_distance = 0;
+ for (auto const& block_key : info->unique_blocks_since_last_access) {
+ auto const& it = block_info_map_.find(block_key);
+ // This block must exist.
+ assert(it != block_info_map_.end());
+ reuse_distance += it->second->block_size;
+ }
+ info->reuse_distance_count[reuse_distance] += 1;
+ // We clear this hash set since this is the second access on this block.
+ info->unique_blocks_since_last_access.clear();
+}
+
+Status BlockCacheTraceAnalyzer::RecordAccess(
+ const BlockCacheTraceRecord& access) {
+ ColumnFamilyAccessInfoAggregate& cf_aggr = cf_aggregates_map_[access.cf_name];
+ SSTFileAccessInfoAggregate& file_aggr =
+ cf_aggr.fd_aggregates_map[access.sst_fd_number];
+ file_aggr.level = access.level;
+ BlockTypeAccessInfoAggregate& block_type_aggr =
+ file_aggr.block_type_aggregates_map[access.block_type];
+ if (block_type_aggr.block_access_info_map.find(access.block_key) ==
+ block_type_aggr.block_access_info_map.end()) {
+ block_type_aggr.block_access_info_map[access.block_key].block_id =
+ unique_block_id_;
+ unique_block_id_++;
+ }
+ BlockAccessInfo& block_access_info =
+ block_type_aggr.block_access_info_map[access.block_key];
+ if (compute_reuse_distance_) {
+ ComputeReuseDistance(&block_access_info);
+ }
+ block_access_info.AddAccess(access, access_sequence_number_);
+ block_info_map_[access.block_key] = &block_access_info;
+ uint64_t get_key_id = 0;
+ if (access.caller == TableReaderCaller::kUserGet &&
+ access.get_id != BlockCacheTraceHelper::kReservedGetId) {
+ std::string user_key = ExtractUserKey(access.referenced_key).ToString();
+ if (get_key_info_map_.find(user_key) == get_key_info_map_.end()) {
+ get_key_info_map_[user_key].key_id = unique_get_key_id_;
+ unique_get_key_id_++;
+ }
+ get_key_id = get_key_info_map_[user_key].key_id;
+ get_key_info_map_[user_key].AddAccess(access, access_sequence_number_);
+ }
+
+ if (compute_reuse_distance_) {
+ // Add this block to all existing blocks.
+ for (auto& cf_aggregates : cf_aggregates_map_) {
+ for (auto& file_aggregates : cf_aggregates.second.fd_aggregates_map) {
+ for (auto& block_type_aggregates :
+ file_aggregates.second.block_type_aggregates_map) {
+ for (auto& existing_block :
+ block_type_aggregates.second.block_access_info_map) {
+ existing_block.second.unique_blocks_since_last_access.insert(
+ access.block_key);
+ }
+ }
+ }
+ }
+ }
+ return human_readable_trace_writer_.WriteHumanReadableTraceRecord(
+ access, block_access_info.block_id, get_key_id);
+}
+
+Status BlockCacheTraceAnalyzer::Analyze() {
+ SystemClock* clock = env_->GetSystemClock().get();
+ std::unique_ptr<BlockCacheTraceReader> reader;
+ Status s = Status::OK();
+ if (is_human_readable_trace_file_) {
+ reader.reset(new BlockCacheHumanReadableTraceReader(trace_file_path_));
+ } else {
+ std::unique_ptr<TraceReader> trace_reader;
+ s = NewFileTraceReader(env_, EnvOptions(), trace_file_path_, &trace_reader);
+ if (!s.ok()) {
+ return s;
+ }
+ reader.reset(new BlockCacheTraceReader(std::move(trace_reader)));
+ s = reader->ReadHeader(&header_);
+ if (!s.ok()) {
+ return s;
+ }
+ }
+ if (!human_readable_trace_file_path_.empty()) {
+ s = human_readable_trace_writer_.NewWritableFile(
+ human_readable_trace_file_path_, env_);
+ if (!s.ok()) {
+ return s;
+ }
+ }
+ uint64_t start = clock->NowMicros();
+ uint64_t time_interval = 0;
+ while (s.ok()) {
+ BlockCacheTraceRecord access;
+ s = reader->ReadAccess(&access);
+ if (!s.ok()) {
+ break;
+ }
+ if (!mrc_only_) {
+ s = RecordAccess(access);
+ if (!s.ok()) {
+ break;
+ }
+ }
+ if (trace_start_timestamp_in_seconds_ == 0) {
+ trace_start_timestamp_in_seconds_ =
+ access.access_timestamp / kMicrosInSecond;
+ }
+ trace_end_timestamp_in_seconds_ = access.access_timestamp / kMicrosInSecond;
+ miss_ratio_stats_.UpdateMetrics(access.access_timestamp,
+ is_user_access(access.caller),
+ !access.is_cache_hit);
+ if (cache_simulator_) {
+ cache_simulator_->Access(access);
+ }
+ access_sequence_number_++;
+ uint64_t now = clock->NowMicros();
+ uint64_t duration = (now - start) / kMicrosInSecond;
+ if (duration > 10 * time_interval) {
+ uint64_t trace_duration =
+ trace_end_timestamp_in_seconds_ - trace_start_timestamp_in_seconds_;
+ fprintf(stdout,
+ "Running for %" PRIu64 " seconds: Processed %" PRIu64
+ " records/second. Trace duration %" PRIu64
+ " seconds. Observed miss ratio %.2f\n",
+ duration, duration > 0 ? access_sequence_number_ / duration : 0,
+ trace_duration, miss_ratio_stats_.miss_ratio());
+ time_interval++;
+ }
+ }
+ uint64_t now = clock->NowMicros();
+ uint64_t duration = (now - start) / kMicrosInSecond;
+ uint64_t trace_duration =
+ trace_end_timestamp_in_seconds_ - trace_start_timestamp_in_seconds_;
+ fprintf(stdout,
+ "Running for %" PRIu64 " seconds: Processed %" PRIu64
+ " records/second. Trace duration %" PRIu64
+ " seconds. Observed miss ratio %.2f\n",
+ duration, duration > 0 ? access_sequence_number_ / duration : 0,
+ trace_duration, miss_ratio_stats_.miss_ratio());
+ return s;
+}
+
+void BlockCacheTraceAnalyzer::PrintBlockSizeStats() const {
+ HistogramStat bs_stats;
+ std::map<TraceType, HistogramStat> bt_stats_map;
+ std::map<std::string, std::map<TraceType, HistogramStat>> cf_bt_stats_map;
+ auto block_callback =
+ [&](const std::string& cf_name, uint64_t /*fd*/, uint32_t /*level*/,
+ TraceType type, const std::string& /*block_key*/,
+ uint64_t /*block_id*/, const BlockAccessInfo& block) {
+ if (block.block_size == 0) {
+ // Block size may be 0 when 1) compaction observes a cache miss and
+ // does not insert the missing block into the cache again. 2)
+ // fetching filter blocks in SST files at the last level.
+ return;
+ }
+ bs_stats.Add(block.block_size);
+ bt_stats_map[type].Add(block.block_size);
+ cf_bt_stats_map[cf_name][type].Add(block.block_size);
+ };
+ TraverseBlocks(block_callback);
+ fprintf(stdout, "Block size stats: \n%s", bs_stats.ToString().c_str());
+ for (auto const& bt_stats : bt_stats_map) {
+ print_break_lines(/*num_break_lines=*/1);
+ fprintf(stdout, "Block size stats for block type %s: \n%s",
+ block_type_to_string(bt_stats.first).c_str(),
+ bt_stats.second.ToString().c_str());
+ }
+ for (auto const& cf_bt_stats : cf_bt_stats_map) {
+ const std::string& cf_name = cf_bt_stats.first;
+ for (auto const& bt_stats : cf_bt_stats.second) {
+ print_break_lines(/*num_break_lines=*/1);
+ fprintf(stdout,
+ "Block size stats for column family %s and block type %s: \n%s",
+ cf_name.c_str(), block_type_to_string(bt_stats.first).c_str(),
+ bt_stats.second.ToString().c_str());
+ }
+ }
+}
+
+void BlockCacheTraceAnalyzer::PrintAccessCountStats(bool user_access_only,
+ uint32_t bottom_k,
+ uint32_t top_k) const {
+ HistogramStat access_stats;
+ std::map<TraceType, HistogramStat> bt_stats_map;
+ std::map<std::string, std::map<TraceType, HistogramStat>> cf_bt_stats_map;
+ std::map<uint64_t, std::vector<std::string>> access_count_blocks;
+ auto block_callback = [&](const std::string& cf_name, uint64_t /*fd*/,
+ uint32_t /*level*/, TraceType type,
+ const std::string& block_key, uint64_t /*block_id*/,
+ const BlockAccessInfo& block) {
+ uint64_t naccesses = 0;
+ for (auto const& caller_access : block.caller_num_access_map) {
+ if (!user_access_only || is_user_access(caller_access.first)) {
+ naccesses += caller_access.second;
+ }
+ }
+ if (naccesses == 0) {
+ return;
+ }
+ if (type == TraceType::kBlockTraceDataBlock) {
+ access_count_blocks[naccesses].push_back(block_key);
+ }
+ access_stats.Add(naccesses);
+ bt_stats_map[type].Add(naccesses);
+ cf_bt_stats_map[cf_name][type].Add(naccesses);
+ };
+ TraverseBlocks(block_callback);
+ fprintf(stdout,
+ "Block access count stats: The number of accesses per block. %s\n%s",
+ user_access_only ? "User accesses only" : "All accesses",
+ access_stats.ToString().c_str());
+ uint32_t bottom_k_index = 0;
+ for (auto naccess_it = access_count_blocks.begin();
+ naccess_it != access_count_blocks.end(); naccess_it++) {
+ bottom_k_index++;
+ if (bottom_k_index >= bottom_k) {
+ break;
+ }
+ std::map<TableReaderCaller, uint64_t> caller_naccesses;
+ uint64_t naccesses = 0;
+ for (auto const& block_id : naccess_it->second) {
+ BlockAccessInfo* block = block_info_map_.find(block_id)->second;
+ for (auto const& caller_access : block->caller_num_access_map) {
+ if (!user_access_only || is_user_access(caller_access.first)) {
+ caller_naccesses[caller_access.first] += caller_access.second;
+ naccesses += caller_access.second;
+ }
+ }
+ }
+ std::string statistics("Caller:");
+ for (auto const& caller_naccessess_it : caller_naccesses) {
+ statistics += caller_to_string(caller_naccessess_it.first);
+ statistics += ":";
+ statistics +=
+ std::to_string(percent(caller_naccessess_it.second, naccesses));
+ statistics += ",";
+ }
+ fprintf(stdout,
+ "Bottom %" PRIu32 " access count. Access count=%" PRIu64
+ " nblocks=%" ROCKSDB_PRIszt " %s\n",
+ bottom_k, naccess_it->first, naccess_it->second.size(),
+ statistics.c_str());
+ }
+
+ uint32_t top_k_index = 0;
+ for (auto naccess_it = access_count_blocks.rbegin();
+ naccess_it != access_count_blocks.rend(); naccess_it++) {
+ top_k_index++;
+ if (top_k_index >= top_k) {
+ break;
+ }
+ for (auto const& block_id : naccess_it->second) {
+ BlockAccessInfo* block = block_info_map_.find(block_id)->second;
+ std::string statistics("Caller:");
+ uint64_t naccesses = 0;
+ for (auto const& caller_access : block->caller_num_access_map) {
+ if (!user_access_only || is_user_access(caller_access.first)) {
+ naccesses += caller_access.second;
+ }
+ }
+ assert(naccesses > 0);
+ for (auto const& caller_access : block->caller_num_access_map) {
+ if (!user_access_only || is_user_access(caller_access.first)) {
+ statistics += ",";
+ statistics += caller_to_string(caller_access.first);
+ statistics += ":";
+ statistics +=
+ std::to_string(percent(caller_access.second, naccesses));
+ }
+ }
+ uint64_t ref_keys_accesses = 0;
+ uint64_t ref_keys_does_not_exist_accesses = 0;
+ for (auto const& ref_key_caller_access : block->key_num_access_map) {
+ for (auto const& caller_access : ref_key_caller_access.second) {
+ if (!user_access_only || is_user_access(caller_access.first)) {
+ ref_keys_accesses += caller_access.second;
+ }
+ }
+ }
+ for (auto const& ref_key_caller_access :
+ block->non_exist_key_num_access_map) {
+ for (auto const& caller_access : ref_key_caller_access.second) {
+ if (!user_access_only || is_user_access(caller_access.first)) {
+ ref_keys_does_not_exist_accesses += caller_access.second;
+ }
+ }
+ }
+ statistics += ",nkeys=";
+ statistics += std::to_string(block->num_keys);
+ statistics += ",block_size=";
+ statistics += std::to_string(block->block_size);
+ statistics += ",num_ref_keys=";
+ statistics += std::to_string(block->key_num_access_map.size());
+ statistics += ",percent_access_ref_keys=";
+ statistics += std::to_string(percent(ref_keys_accesses, naccesses));
+ statistics += ",num_ref_keys_does_not_exist=";
+ statistics += std::to_string(block->non_exist_key_num_access_map.size());
+ statistics += ",percent_access_ref_keys_does_not_exist=";
+ statistics +=
+ std::to_string(percent(ref_keys_does_not_exist_accesses, naccesses));
+ statistics += ",ref_data_size=";
+ statistics += std::to_string(block->referenced_data_size);
+ fprintf(stdout,
+ "Top %" PRIu32 " access count blocks access_count=%" PRIu64
+ " %s\n",
+ top_k, naccess_it->first, statistics.c_str());
+ }
+ }
+
+ for (auto const& bt_stats : bt_stats_map) {
+ print_break_lines(/*num_break_lines=*/1);
+ fprintf(stdout, "Break down by block type %s: \n%s",
+ block_type_to_string(bt_stats.first).c_str(),
+ bt_stats.second.ToString().c_str());
+ }
+ for (auto const& cf_bt_stats : cf_bt_stats_map) {
+ const std::string& cf_name = cf_bt_stats.first;
+ for (auto const& bt_stats : cf_bt_stats.second) {
+ print_break_lines(/*num_break_lines=*/1);
+ fprintf(stdout,
+ "Break down by column family %s and block type "
+ "%s: \n%s",
+ cf_name.c_str(), block_type_to_string(bt_stats.first).c_str(),
+ bt_stats.second.ToString().c_str());
+ }
+ }
+}
+
+void BlockCacheTraceAnalyzer::PrintDataBlockAccessStats() const {
+ HistogramStat existing_keys_stats;
+ std::map<std::string, HistogramStat> cf_existing_keys_stats_map;
+ HistogramStat non_existing_keys_stats;
+ std::map<std::string, HistogramStat> cf_non_existing_keys_stats_map;
+ HistogramStat block_access_stats;
+ std::map<std::string, HistogramStat> cf_block_access_info;
+ HistogramStat percent_referenced_bytes;
+ std::map<std::string, HistogramStat> cf_percent_referenced_bytes;
+ // Total number of accesses in a data block / number of keys in a data block.
+ HistogramStat avg_naccesses_per_key_in_a_data_block;
+ std::map<std::string, HistogramStat> cf_avg_naccesses_per_key_in_a_data_block;
+ // The standard deviation on the number of accesses of a key in a data block.
+ HistogramStat stdev_naccesses_per_key_in_a_data_block;
+ std::map<std::string, HistogramStat>
+ cf_stdev_naccesses_per_key_in_a_data_block;
+ auto block_callback =
+ [&](const std::string& cf_name, uint64_t /*fd*/, uint32_t /*level*/,
+ TraceType /*type*/, const std::string& /*block_key*/,
+ uint64_t /*block_id*/, const BlockAccessInfo& block) {
+ if (block.num_keys == 0) {
+ return;
+ }
+ // Use four decimal points.
+ uint64_t percent_referenced_for_existing_keys =
+ (uint64_t)(((double)block.key_num_access_map.size() /
+ (double)block.num_keys) *
+ 10000.0);
+ uint64_t percent_referenced_for_non_existing_keys =
+ (uint64_t)(((double)block.non_exist_key_num_access_map.size() /
+ (double)block.num_keys) *
+ 10000.0);
+ uint64_t percent_accesses_for_existing_keys =
+ (uint64_t)(((double)block.num_referenced_key_exist_in_block /
+ (double)block.num_accesses) *
+ 10000.0);
+
+ HistogramStat hist_naccess_per_key;
+ for (auto const& key_access : block.key_num_access_map) {
+ for (auto const& caller_access : key_access.second) {
+ hist_naccess_per_key.Add(caller_access.second);
+ }
+ }
+ uint64_t avg_accesses =
+ static_cast<uint64_t>(hist_naccess_per_key.Average());
+ uint64_t stdev_accesses =
+ static_cast<uint64_t>(hist_naccess_per_key.StandardDeviation());
+ avg_naccesses_per_key_in_a_data_block.Add(avg_accesses);
+ cf_avg_naccesses_per_key_in_a_data_block[cf_name].Add(avg_accesses);
+ stdev_naccesses_per_key_in_a_data_block.Add(stdev_accesses);
+ cf_stdev_naccesses_per_key_in_a_data_block[cf_name].Add(stdev_accesses);
+
+ existing_keys_stats.Add(percent_referenced_for_existing_keys);
+ cf_existing_keys_stats_map[cf_name].Add(
+ percent_referenced_for_existing_keys);
+ non_existing_keys_stats.Add(percent_referenced_for_non_existing_keys);
+ cf_non_existing_keys_stats_map[cf_name].Add(
+ percent_referenced_for_non_existing_keys);
+ block_access_stats.Add(percent_accesses_for_existing_keys);
+ cf_block_access_info[cf_name].Add(percent_accesses_for_existing_keys);
+ };
+ TraverseBlocks(block_callback);
+ fprintf(stdout,
+ "Histogram on the number of referenced keys existing in a block over "
+ "the total number of keys in a block: \n%s",
+ existing_keys_stats.ToString().c_str());
+ for (auto const& cf_stats : cf_existing_keys_stats_map) {
+ print_break_lines(/*num_break_lines=*/1);
+ fprintf(stdout, "Break down by column family %s: \n%s",
+ cf_stats.first.c_str(), cf_stats.second.ToString().c_str());
+ }
+ print_break_lines(/*num_break_lines=*/1);
+ fprintf(
+ stdout,
+ "Histogram on the number of referenced keys DO NOT exist in a block over "
+ "the total number of keys in a block: \n%s",
+ non_existing_keys_stats.ToString().c_str());
+ for (auto const& cf_stats : cf_non_existing_keys_stats_map) {
+ print_break_lines(/*num_break_lines=*/1);
+ fprintf(stdout, "Break down by column family %s: \n%s",
+ cf_stats.first.c_str(), cf_stats.second.ToString().c_str());
+ }
+ print_break_lines(/*num_break_lines=*/1);
+ fprintf(stdout,
+ "Histogram on the number of accesses on keys exist in a block over "
+ "the total number of accesses in a block: \n%s",
+ block_access_stats.ToString().c_str());
+ for (auto const& cf_stats : cf_block_access_info) {
+ print_break_lines(/*num_break_lines=*/1);
+ fprintf(stdout, "Break down by column family %s: \n%s",
+ cf_stats.first.c_str(), cf_stats.second.ToString().c_str());
+ }
+ print_break_lines(/*num_break_lines=*/1);
+ fprintf(
+ stdout,
+ "Histogram on the average number of accesses per key in a block: \n%s",
+ avg_naccesses_per_key_in_a_data_block.ToString().c_str());
+ for (auto const& cf_stats : cf_avg_naccesses_per_key_in_a_data_block) {
+ fprintf(stdout, "Break down by column family %s: \n%s",
+ cf_stats.first.c_str(), cf_stats.second.ToString().c_str());
+ }
+ print_break_lines(/*num_break_lines=*/1);
+ fprintf(stdout,
+ "Histogram on the standard deviation of the number of accesses per "
+ "key in a block: \n%s",
+ stdev_naccesses_per_key_in_a_data_block.ToString().c_str());
+ for (auto const& cf_stats : cf_stdev_naccesses_per_key_in_a_data_block) {
+ fprintf(stdout, "Break down by column family %s: \n%s",
+ cf_stats.first.c_str(), cf_stats.second.ToString().c_str());
+ }
+}
+
+void BlockCacheTraceAnalyzer::PrintStatsSummary() const {
+ uint64_t total_num_files = 0;
+ uint64_t total_num_blocks = 0;
+ uint64_t total_num_accesses = 0;
+ std::map<TraceType, uint64_t> bt_num_blocks_map;
+ std::map<TableReaderCaller, uint64_t> caller_num_access_map;
+ std::map<TableReaderCaller, std::map<TraceType, uint64_t>>
+ caller_bt_num_access_map;
+ std::map<TableReaderCaller, std::map<uint32_t, uint64_t>>
+ caller_level_num_access_map;
+ for (auto const& cf_aggregates : cf_aggregates_map_) {
+ // Stats per column family.
+ const std::string& cf_name = cf_aggregates.first;
+ uint64_t cf_num_files = 0;
+ uint64_t cf_num_blocks = 0;
+ std::map<TraceType, uint64_t> cf_bt_blocks;
+ uint64_t cf_num_accesses = 0;
+ std::map<TableReaderCaller, uint64_t> cf_caller_num_accesses_map;
+ std::map<TableReaderCaller, std::map<uint64_t, uint64_t>>
+ cf_caller_level_num_accesses_map;
+ std::map<TableReaderCaller, std::map<uint64_t, uint64_t>>
+ cf_caller_file_num_accesses_map;
+ std::map<TableReaderCaller, std::map<TraceType, uint64_t>>
+ cf_caller_bt_num_accesses_map;
+ total_num_files += cf_aggregates.second.fd_aggregates_map.size();
+ for (auto const& file_aggregates : cf_aggregates.second.fd_aggregates_map) {
+ // Stats per SST file.
+ const uint64_t fd = file_aggregates.first;
+ const uint32_t level = file_aggregates.second.level;
+ cf_num_files++;
+ for (auto const& block_type_aggregates :
+ file_aggregates.second.block_type_aggregates_map) {
+ // Stats per block type.
+ const TraceType type = block_type_aggregates.first;
+ cf_bt_blocks[type] +=
+ block_type_aggregates.second.block_access_info_map.size();
+ total_num_blocks +=
+ block_type_aggregates.second.block_access_info_map.size();
+ bt_num_blocks_map[type] +=
+ block_type_aggregates.second.block_access_info_map.size();
+ for (auto const& block_access_info :
+ block_type_aggregates.second.block_access_info_map) {
+ // Stats per block.
+ cf_num_blocks++;
+ for (auto const& stats :
+ block_access_info.second.caller_num_access_map) {
+ // Stats per caller.
+ const TableReaderCaller caller = stats.first;
+ const uint64_t num_accesses = stats.second;
+ // Overall stats.
+ total_num_accesses += num_accesses;
+ caller_num_access_map[caller] += num_accesses;
+ caller_bt_num_access_map[caller][type] += num_accesses;
+ caller_level_num_access_map[caller][level] += num_accesses;
+ // Column Family stats.
+ cf_num_accesses += num_accesses;
+ cf_caller_num_accesses_map[caller] += num_accesses;
+ cf_caller_level_num_accesses_map[caller][level] += num_accesses;
+ cf_caller_file_num_accesses_map[caller][fd] += num_accesses;
+ cf_caller_bt_num_accesses_map[caller][type] += num_accesses;
+ }
+ }
+ }
+ }
+
+ // Print stats.
+ print_break_lines(/*num_break_lines=*/3);
+ fprintf(stdout, "Statistics for column family %s:\n", cf_name.c_str());
+ fprintf(stdout,
+ " Number of files:%" PRIu64 " Number of blocks: %" PRIu64
+ " Number of accesses: %" PRIu64 "\n",
+ cf_num_files, cf_num_blocks, cf_num_accesses);
+ for (auto block_type : cf_bt_blocks) {
+ fprintf(stdout, "Number of %s blocks: %" PRIu64 " Percent: %.2f\n",
+ block_type_to_string(block_type.first).c_str(), block_type.second,
+ percent(block_type.second, cf_num_blocks));
+ }
+ for (auto caller : cf_caller_num_accesses_map) {
+ const uint64_t naccesses = caller.second;
+ print_break_lines(/*num_break_lines=*/1);
+ fprintf(stdout,
+ "Caller %s: Number of accesses %" PRIu64 " Percent: %.2f\n",
+ caller_to_string(caller.first).c_str(), naccesses,
+ percent(naccesses, cf_num_accesses));
+ fprintf(stdout, "Caller %s: Number of accesses per level break down\n",
+ caller_to_string(caller.first).c_str());
+ for (auto naccess_level :
+ cf_caller_level_num_accesses_map[caller.first]) {
+ fprintf(stdout,
+ "\t Level %" PRIu64 ": Number of accesses: %" PRIu64
+ " Percent: %.2f\n",
+ naccess_level.first, naccess_level.second,
+ percent(naccess_level.second, naccesses));
+ }
+ fprintf(stdout, "Caller %s: Number of accesses per file break down\n",
+ caller_to_string(caller.first).c_str());
+ for (auto naccess_file : cf_caller_file_num_accesses_map[caller.first]) {
+ fprintf(stdout,
+ "\t File %" PRIu64 ": Number of accesses: %" PRIu64
+ " Percent: %.2f\n",
+ naccess_file.first, naccess_file.second,
+ percent(naccess_file.second, naccesses));
+ }
+ fprintf(stdout,
+ "Caller %s: Number of accesses per block type break down\n",
+ caller_to_string(caller.first).c_str());
+ for (auto naccess_type : cf_caller_bt_num_accesses_map[caller.first]) {
+ fprintf(stdout,
+ "\t Block Type %s: Number of accesses: %" PRIu64
+ " Percent: %.2f\n",
+ block_type_to_string(naccess_type.first).c_str(),
+ naccess_type.second, percent(naccess_type.second, naccesses));
+ }
+ }
+ }
+ print_break_lines(/*num_break_lines=*/3);
+ fprintf(stdout, "Overall statistics:\n");
+ fprintf(stdout,
+ "Number of files: %" PRIu64 " Number of blocks: %" PRIu64
+ " Number of accesses: %" PRIu64 "\n",
+ total_num_files, total_num_blocks, total_num_accesses);
+ for (auto block_type : bt_num_blocks_map) {
+ fprintf(stdout, "Number of %s blocks: %" PRIu64 " Percent: %.2f\n",
+ block_type_to_string(block_type.first).c_str(), block_type.second,
+ percent(block_type.second, total_num_blocks));
+ }
+ for (auto caller : caller_num_access_map) {
+ print_break_lines(/*num_break_lines=*/1);
+ uint64_t naccesses = caller.second;
+ fprintf(stdout, "Caller %s: Number of accesses %" PRIu64 " Percent: %.2f\n",
+ caller_to_string(caller.first).c_str(), naccesses,
+ percent(naccesses, total_num_accesses));
+ fprintf(stdout, "Caller %s: Number of accesses per level break down\n",
+ caller_to_string(caller.first).c_str());
+ for (auto naccess_level : caller_level_num_access_map[caller.first]) {
+ fprintf(stdout,
+ "\t Level %d: Number of accesses: %" PRIu64 " Percent: %.2f\n",
+ naccess_level.first, naccess_level.second,
+ percent(naccess_level.second, naccesses));
+ }
+ fprintf(stdout, "Caller %s: Number of accesses per block type break down\n",
+ caller_to_string(caller.first).c_str());
+ for (auto naccess_type : caller_bt_num_access_map[caller.first]) {
+ fprintf(stdout,
+ "\t Block Type %s: Number of accesses: %" PRIu64
+ " Percent: %.2f\n",
+ block_type_to_string(naccess_type.first).c_str(),
+ naccess_type.second, percent(naccess_type.second, naccesses));
+ }
+ }
+}
+
+std::vector<CacheConfiguration> parse_cache_config_file(
+ const std::string& config_path) {
+ std::ifstream file(config_path);
+ if (!file.is_open()) {
+ return {};
+ }
+ std::vector<CacheConfiguration> configs;
+ std::string line;
+ while (getline(file, line)) {
+ CacheConfiguration cache_config;
+ std::stringstream ss(line);
+ std::vector<std::string> config_strs;
+ while (ss.good()) {
+ std::string substr;
+ getline(ss, substr, ',');
+ config_strs.push_back(substr);
+ }
+ // Sanity checks.
+ if (config_strs.size() < 4) {
+ fprintf(stderr, "Invalid cache simulator configuration %s\n",
+ line.c_str());
+ exit(1);
+ }
+ if (kSupportedCacheNames.find(" " + config_strs[0] + " ") ==
+ std::string::npos) {
+ fprintf(stderr, "Invalid cache name %s. Supported cache names are %s\n",
+ line.c_str(), kSupportedCacheNames.c_str());
+ exit(1);
+ }
+ cache_config.cache_name = config_strs[0];
+ cache_config.num_shard_bits = ParseUint32(config_strs[1]);
+ cache_config.ghost_cache_capacity = ParseUint64(config_strs[2]);
+ for (uint32_t i = 3; i < config_strs.size(); i++) {
+ uint64_t capacity = ParseUint64(config_strs[i]);
+ if (capacity == 0) {
+ fprintf(stderr, "Invalid cache capacity %s, %s\n",
+ config_strs[i].c_str(), line.c_str());
+ exit(1);
+ }
+ cache_config.cache_capacities.push_back(capacity);
+ }
+ configs.push_back(cache_config);
+ }
+ file.close();
+ return configs;
+}
+
+std::vector<uint64_t> parse_buckets(const std::string& bucket_str) {
+ std::vector<uint64_t> buckets;
+ std::stringstream ss(bucket_str);
+ while (ss.good()) {
+ std::string bucket;
+ getline(ss, bucket, ',');
+ buckets.push_back(ParseUint64(bucket));
+ }
+ buckets.push_back(std::numeric_limits<uint64_t>::max());
+ return buckets;
+}
+
+int block_cache_trace_analyzer_tool(int argc, char** argv) {
+ ParseCommandLineFlags(&argc, &argv, true);
+ if (FLAGS_block_cache_trace_path.empty()) {
+ fprintf(stderr, "block cache trace path is empty\n");
+ exit(1);
+ }
+ uint64_t warmup_seconds =
+ FLAGS_cache_sim_warmup_seconds > 0 ? FLAGS_cache_sim_warmup_seconds : 0;
+ uint32_t downsample_ratio = FLAGS_block_cache_trace_downsample_ratio > 0
+ ? FLAGS_block_cache_trace_downsample_ratio
+ : 0;
+ std::vector<CacheConfiguration> cache_configs =
+ parse_cache_config_file(FLAGS_block_cache_sim_config_path);
+ std::unique_ptr<BlockCacheTraceSimulator> cache_simulator;
+ if (!cache_configs.empty()) {
+ cache_simulator.reset(new BlockCacheTraceSimulator(
+ warmup_seconds, downsample_ratio, cache_configs));
+ Status s = cache_simulator->InitializeCaches();
+ if (!s.ok()) {
+ fprintf(stderr, "Cannot initialize cache simulators %s\n",
+ s.ToString().c_str());
+ exit(1);
+ }
+ }
+ BlockCacheTraceAnalyzer analyzer(
+ FLAGS_block_cache_trace_path, FLAGS_block_cache_analysis_result_dir,
+ FLAGS_human_readable_trace_file_path,
+ !FLAGS_reuse_distance_labels.empty(), FLAGS_mrc_only,
+ FLAGS_is_block_cache_human_readable_trace, std::move(cache_simulator));
+ Status s = analyzer.Analyze();
+ if (!s.IsIncomplete() && !s.ok()) {
+ // Read all traces.
+ fprintf(stderr, "Cannot process the trace %s\n", s.ToString().c_str());
+ exit(1);
+ }
+ fprintf(stdout, "Status: %s\n", s.ToString().c_str());
+ analyzer.WriteMissRatioCurves();
+ analyzer.WriteMissRatioTimeline(1);
+ analyzer.WriteMissRatioTimeline(kSecondInMinute);
+ analyzer.WriteMissRatioTimeline(kSecondInHour);
+ analyzer.WriteMissTimeline(1);
+ analyzer.WriteMissTimeline(kSecondInMinute);
+ analyzer.WriteMissTimeline(kSecondInHour);
+
+ if (FLAGS_mrc_only) {
+ fprintf(stdout,
+ "Skipping the analysis statistics since the user wants to compute "
+ "MRC only");
+ return 0;
+ }
+
+ analyzer.PrintStatsSummary();
+ if (FLAGS_print_access_count_stats) {
+ print_break_lines(/*num_break_lines=*/3);
+ analyzer.PrintAccessCountStats(
+ /*user_access_only=*/false, FLAGS_analyze_bottom_k_access_count_blocks,
+ FLAGS_analyze_top_k_access_count_blocks);
+ print_break_lines(/*num_break_lines=*/3);
+ analyzer.PrintAccessCountStats(
+ /*user_access_only=*/true, FLAGS_analyze_bottom_k_access_count_blocks,
+ FLAGS_analyze_top_k_access_count_blocks);
+ }
+ if (FLAGS_print_block_size_stats) {
+ print_break_lines(/*num_break_lines=*/3);
+ analyzer.PrintBlockSizeStats();
+ }
+ if (FLAGS_print_data_block_access_count_stats) {
+ print_break_lines(/*num_break_lines=*/3);
+ analyzer.PrintDataBlockAccessStats();
+ }
+ print_break_lines(/*num_break_lines=*/3);
+
+ if (!FLAGS_timeline_labels.empty()) {
+ std::stringstream ss(FLAGS_timeline_labels);
+ while (ss.good()) {
+ std::string label;
+ getline(ss, label, ',');
+ if (label.find("block") != std::string::npos) {
+ analyzer.WriteAccessTimeline(label, kSecondInMinute, true);
+ analyzer.WriteAccessTimeline(label, kSecondInMinute, false);
+ analyzer.WriteAccessTimeline(label, kSecondInHour, true);
+ analyzer.WriteAccessTimeline(label, kSecondInHour, false);
+ } else {
+ analyzer.WriteAccessTimeline(label, kSecondInMinute, false);
+ analyzer.WriteAccessTimeline(label, kSecondInHour, false);
+ }
+ }
+ }
+
+ if (!FLAGS_analyze_callers.empty()) {
+ analyzer.WritePercentAccessSummaryStats();
+ std::stringstream ss(FLAGS_analyze_callers);
+ while (ss.good()) {
+ std::string caller;
+ getline(ss, caller, ',');
+ analyzer.WriteDetailedPercentAccessSummaryStats(string_to_caller(caller));
+ }
+ }
+
+ if (!FLAGS_access_count_buckets.empty()) {
+ std::vector<uint64_t> buckets = parse_buckets(FLAGS_access_count_buckets);
+ analyzer.WriteAccessCountSummaryStats(buckets, /*user_access_only=*/true);
+ analyzer.WriteAccessCountSummaryStats(buckets, /*user_access_only=*/false);
+ }
+
+ if (!FLAGS_reuse_distance_labels.empty() &&
+ !FLAGS_reuse_distance_buckets.empty()) {
+ std::vector<uint64_t> buckets = parse_buckets(FLAGS_reuse_distance_buckets);
+ std::stringstream ss(FLAGS_reuse_distance_labels);
+ while (ss.good()) {
+ std::string label;
+ getline(ss, label, ',');
+ analyzer.WriteReuseDistance(label, buckets);
+ }
+ }
+
+ if (!FLAGS_reuse_interval_labels.empty() &&
+ !FLAGS_reuse_interval_buckets.empty()) {
+ std::vector<uint64_t> buckets = parse_buckets(FLAGS_reuse_interval_buckets);
+ std::stringstream ss(FLAGS_reuse_interval_labels);
+ while (ss.good()) {
+ std::string label;
+ getline(ss, label, ',');
+ analyzer.WriteReuseInterval(label, buckets);
+ }
+ }
+
+ if (!FLAGS_reuse_lifetime_labels.empty() &&
+ !FLAGS_reuse_lifetime_buckets.empty()) {
+ std::vector<uint64_t> buckets = parse_buckets(FLAGS_reuse_lifetime_buckets);
+ std::stringstream ss(FLAGS_reuse_lifetime_labels);
+ while (ss.good()) {
+ std::string label;
+ getline(ss, label, ',');
+ analyzer.WriteReuseLifetime(label, buckets);
+ }
+ }
+
+ if (FLAGS_analyze_blocks_reuse_k_reuse_window != 0) {
+ std::vector<TraceType> block_types{TraceType::kBlockTraceIndexBlock,
+ TraceType::kBlockTraceDataBlock,
+ TraceType::kBlockTraceFilterBlock};
+ for (auto block_type : block_types) {
+ analyzer.WriteBlockReuseTimeline(
+ FLAGS_analyze_blocks_reuse_k_reuse_window,
+ /*user_access_only=*/true, block_type);
+ analyzer.WriteBlockReuseTimeline(
+ FLAGS_analyze_blocks_reuse_k_reuse_window,
+ /*user_access_only=*/false, block_type);
+ }
+ }
+
+ if (!FLAGS_analyze_get_spatial_locality_labels.empty() &&
+ !FLAGS_analyze_get_spatial_locality_buckets.empty()) {
+ std::vector<uint64_t> buckets =
+ parse_buckets(FLAGS_analyze_get_spatial_locality_buckets);
+ std::stringstream ss(FLAGS_analyze_get_spatial_locality_labels);
+ while (ss.good()) {
+ std::string label;
+ getline(ss, label, ',');
+ analyzer.WriteGetSpatialLocality(label, buckets);
+ }
+ }
+
+ if (!FLAGS_analyze_correlation_coefficients_labels.empty()) {
+ std::stringstream ss(FLAGS_analyze_correlation_coefficients_labels);
+ while (ss.good()) {
+ std::string label;
+ getline(ss, label, ',');
+ analyzer.WriteCorrelationFeatures(
+ label, FLAGS_analyze_correlation_coefficients_max_number_of_values);
+ }
+ analyzer.WriteCorrelationFeaturesForGet(
+ FLAGS_analyze_correlation_coefficients_max_number_of_values);
+ }
+
+ if (!FLAGS_skew_labels.empty() && !FLAGS_skew_buckets.empty()) {
+ std::vector<uint64_t> buckets = parse_buckets(FLAGS_skew_buckets);
+ std::stringstream ss(FLAGS_skew_labels);
+ while (ss.good()) {
+ std::string label;
+ getline(ss, label, ',');
+ if (label.find("block") != std::string::npos) {
+ analyzer.WriteSkewness(label, buckets,
+ TraceType::kBlockTraceIndexBlock);
+ analyzer.WriteSkewness(label, buckets,
+ TraceType::kBlockTraceFilterBlock);
+ analyzer.WriteSkewness(label, buckets, TraceType::kBlockTraceDataBlock);
+ analyzer.WriteSkewness(label, buckets, TraceType::kTraceMax);
+ } else {
+ analyzer.WriteSkewness(label, buckets, TraceType::kTraceMax);
+ }
+ }
+ }
+ return 0;
+}
+
+} // namespace ROCKSDB_NAMESPACE
+
+#endif // GFLAGS
+#endif // ROCKSDB_LITE
diff --git a/src/rocksdb/tools/block_cache_analyzer/block_cache_trace_analyzer.h b/src/rocksdb/tools/block_cache_analyzer/block_cache_trace_analyzer.h
new file mode 100644
index 000000000..2f1ebd139
--- /dev/null
+++ b/src/rocksdb/tools/block_cache_analyzer/block_cache_trace_analyzer.h
@@ -0,0 +1,397 @@
+// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+// This source code is licensed under both the GPLv2 (found in the
+// COPYING file in the root directory) and Apache 2.0 License
+// (found in the LICENSE.Apache file in the root directory).
+
+#pragma once
+
+#include <map>
+#include <set>
+#include <vector>
+
+#include "db/dbformat.h"
+#include "rocksdb/env.h"
+#include "rocksdb/trace_record.h"
+#include "rocksdb/utilities/sim_cache.h"
+#include "trace_replay/block_cache_tracer.h"
+#include "utilities/simulator_cache/cache_simulator.h"
+
+namespace ROCKSDB_NAMESPACE {
+
+// Statistics of a key refereneced by a Get.
+struct GetKeyInfo {
+ uint64_t key_id = 0;
+ std::vector<uint64_t> access_sequence_number_timeline;
+ std::vector<uint64_t> access_timeline;
+
+ void AddAccess(const BlockCacheTraceRecord& access,
+ uint64_t access_sequnce_number) {
+ access_sequence_number_timeline.push_back(access_sequnce_number);
+ access_timeline.push_back(access.access_timestamp);
+ }
+};
+
+// Statistics of a block.
+struct BlockAccessInfo {
+ uint64_t block_id = 0;
+ uint64_t table_id = 0;
+ uint64_t block_offset = 0;
+ uint64_t num_accesses = 0;
+ uint64_t block_size = 0;
+ uint64_t first_access_time = 0;
+ uint64_t last_access_time = 0;
+ uint64_t num_keys = 0;
+ std::map<std::string, std::map<TableReaderCaller, uint64_t>>
+ key_num_access_map; // for keys exist in this block.
+ std::map<std::string, std::map<TableReaderCaller, uint64_t>>
+ non_exist_key_num_access_map; // for keys do not exist in this block.
+ uint64_t num_referenced_key_exist_in_block = 0;
+ uint64_t referenced_data_size = 0;
+ std::map<TableReaderCaller, uint64_t> caller_num_access_map;
+ // caller:timestamp:number_of_accesses. The granularity of the timestamp is
+ // seconds.
+ std::map<TableReaderCaller, std::map<uint64_t, uint64_t>>
+ caller_num_accesses_timeline;
+ // Unique blocks since the last access.
+ std::set<std::string> unique_blocks_since_last_access;
+ // Number of reuses grouped by reuse distance.
+ std::map<uint64_t, uint64_t> reuse_distance_count;
+
+ // The access sequence numbers of this block.
+ std::vector<uint64_t> access_sequence_number_timeline;
+ std::map<TableReaderCaller, std::vector<uint64_t>>
+ caller_access_sequence__number_timeline;
+ // The access timestamp in microseconds of this block.
+ std::vector<uint64_t> access_timeline;
+ std::map<TableReaderCaller, std::vector<uint64_t>> caller_access_timeline;
+
+ void AddAccess(const BlockCacheTraceRecord& access,
+ uint64_t access_sequnce_number) {
+ if (block_size != 0 && access.block_size != 0) {
+ assert(block_size == access.block_size);
+ }
+ if (num_keys != 0 && access.num_keys_in_block != 0) {
+ assert(num_keys == access.num_keys_in_block);
+ }
+ if (first_access_time == 0) {
+ first_access_time = access.access_timestamp;
+ }
+ table_id = BlockCacheTraceHelper::GetTableId(access);
+ block_offset = BlockCacheTraceHelper::GetBlockOffsetInFile(access);
+ last_access_time = access.access_timestamp;
+ block_size = access.block_size;
+ caller_num_access_map[access.caller]++;
+ num_accesses++;
+ // access.access_timestamp is in microsecond.
+ const uint64_t timestamp_in_seconds =
+ access.access_timestamp / kMicrosInSecond;
+ caller_num_accesses_timeline[access.caller][timestamp_in_seconds] += 1;
+ // Populate the feature vectors.
+ access_sequence_number_timeline.push_back(access_sequnce_number);
+ caller_access_sequence__number_timeline[access.caller].push_back(
+ access_sequnce_number);
+ access_timeline.push_back(access.access_timestamp);
+ caller_access_timeline[access.caller].push_back(access.access_timestamp);
+ if (BlockCacheTraceHelper::IsGetOrMultiGetOnDataBlock(access.block_type,
+ access.caller)) {
+ num_keys = access.num_keys_in_block;
+ if (access.referenced_key_exist_in_block) {
+ if (key_num_access_map.find(access.referenced_key) ==
+ key_num_access_map.end()) {
+ referenced_data_size += access.referenced_data_size;
+ }
+ key_num_access_map[access.referenced_key][access.caller]++;
+ num_referenced_key_exist_in_block++;
+ if (referenced_data_size > block_size && block_size != 0) {
+ ParsedInternalKey internal_key;
+ Status s = ParseInternalKey(access.referenced_key, &internal_key,
+ false /* log_err_key */); // TODO
+ assert(s.ok()); // TODO
+ }
+ } else {
+ non_exist_key_num_access_map[access.referenced_key][access.caller]++;
+ }
+ }
+ }
+};
+
+// Aggregates stats of a block given a block type.
+struct BlockTypeAccessInfoAggregate {
+ std::map<std::string, BlockAccessInfo> block_access_info_map;
+};
+
+// Aggregates BlockTypeAggregate given a SST file.
+struct SSTFileAccessInfoAggregate {
+ uint32_t level;
+ std::map<TraceType, BlockTypeAccessInfoAggregate> block_type_aggregates_map;
+};
+
+// Aggregates SSTFileAggregate given a column family.
+struct ColumnFamilyAccessInfoAggregate {
+ std::map<uint64_t, SSTFileAccessInfoAggregate> fd_aggregates_map;
+};
+
+struct Features {
+ std::vector<uint64_t> elapsed_time_since_last_access;
+ std::vector<uint64_t> num_accesses_since_last_access;
+ std::vector<uint64_t> num_past_accesses;
+};
+
+struct Predictions {
+ std::vector<uint64_t> elapsed_time_till_next_access;
+ std::vector<uint64_t> num_accesses_till_next_access;
+};
+
+class BlockCacheTraceAnalyzer {
+ public:
+ BlockCacheTraceAnalyzer(
+ const std::string& trace_file_path, const std::string& output_dir,
+ const std::string& human_readable_trace_file_path,
+ bool compute_reuse_distance, bool mrc_only,
+ bool is_human_readable_trace_file,
+ std::unique_ptr<BlockCacheTraceSimulator>&& cache_simulator);
+ ~BlockCacheTraceAnalyzer() = default;
+ // No copy and move.
+ BlockCacheTraceAnalyzer(const BlockCacheTraceAnalyzer&) = delete;
+ BlockCacheTraceAnalyzer& operator=(const BlockCacheTraceAnalyzer&) = delete;
+ BlockCacheTraceAnalyzer(BlockCacheTraceAnalyzer&&) = delete;
+ BlockCacheTraceAnalyzer& operator=(BlockCacheTraceAnalyzer&&) = delete;
+
+ // Read all access records in the given trace_file, maintains the stats of
+ // a block, and aggregates the information by block type, sst file, and column
+ // family. Subsequently, the caller may call Print* functions to print
+ // statistics.
+ Status Analyze();
+
+ // Print a summary of statistics of the trace, e.g.,
+ // Number of files: 2 Number of blocks: 50 Number of accesses: 50
+ // Number of Index blocks: 10
+ // Number of Filter blocks: 10
+ // Number of Data blocks: 10
+ // Number of UncompressionDict blocks: 10
+ // Number of RangeDeletion blocks: 10
+ // ***************************************************************
+ // Caller Get: Number of accesses 10
+ // Caller Get: Number of accesses per level break down
+ // Level 0: Number of accesses: 10
+ // Caller Get: Number of accesses per block type break down
+ // Block Type Index: Number of accesses: 2
+ // Block Type Filter: Number of accesses: 2
+ // Block Type Data: Number of accesses: 2
+ // Block Type UncompressionDict: Number of accesses: 2
+ // Block Type RangeDeletion: Number of accesses: 2
+ void PrintStatsSummary() const;
+
+ // Print block size distribution and the distribution break down by block type
+ // and column family.
+ void PrintBlockSizeStats() const;
+
+ // Print access count distribution and the distribution break down by block
+ // type and column family.
+ void PrintAccessCountStats(bool user_access_only, uint32_t bottom_k,
+ uint32_t top_k) const;
+
+ // Print data block accesses by user Get and Multi-Get.
+ // It prints out 1) A histogram on the percentage of keys accessed in a data
+ // block break down by if a referenced key exists in the data block andthe
+ // histogram break down by column family. 2) A histogram on the percentage of
+ // accesses on keys exist in a data block and its break down by column family.
+ void PrintDataBlockAccessStats() const;
+
+ // Write the percentage of accesses break down by column family into a csv
+ // file saved in 'output_dir'.
+ //
+ // The file is named "percentage_of_accesses_summary". The file format is
+ // caller,cf_0,cf_1,...,cf_n where the cf_i is the column family name found in
+ // the trace.
+ void WritePercentAccessSummaryStats() const;
+
+ // Write the percentage of accesses for the given caller break down by column
+ // family, level, and block type into a csv file saved in 'output_dir'.
+ //
+ // It generates two files: 1) caller_level_percentage_of_accesses_summary and
+ // 2) caller_bt_percentage_of_accesses_summary which break down by the level
+ // and block type, respectively. The file format is
+ // level/bt,cf_0,cf_1,...,cf_n where cf_i is the column family name found in
+ // the trace.
+ void WriteDetailedPercentAccessSummaryStats(TableReaderCaller caller) const;
+
+ // Write the access count summary into a csv file saved in 'output_dir'.
+ // It groups blocks by their access count.
+ //
+ // It generates two files: 1) cf_access_count_summary and 2)
+ // bt_access_count_summary which break down the access count by column family
+ // and block type, respectively. The file format is
+ // cf/bt,bucket_0,bucket_1,...,bucket_N.
+ void WriteAccessCountSummaryStats(
+ const std::vector<uint64_t>& access_count_buckets,
+ bool user_access_only) const;
+
+ // Write miss ratio curves of simulated cache configurations into a csv file
+ // named "mrc" saved in 'output_dir'.
+ //
+ // The file format is
+ // "cache_name,num_shard_bits,capacity,miss_ratio,total_accesses".
+ void WriteMissRatioCurves() const;
+
+ // Write miss ratio timeline of simulated cache configurations into several
+ // csv files, one per cache capacity saved in 'output_dir'.
+ //
+ // The file format is
+ // "time,label_1_access_per_second,label_2_access_per_second,...,label_N_access_per_second"
+ // where N is the number of unique cache names
+ // (cache_name+num_shard_bits+ghost_capacity).
+ void WriteMissRatioTimeline(uint64_t time_unit) const;
+
+ // Write misses timeline of simulated cache configurations into several
+ // csv files, one per cache capacity saved in 'output_dir'.
+ //
+ // The file format is
+ // "time,label_1_access_per_second,label_2_access_per_second,...,label_N_access_per_second"
+ // where N is the number of unique cache names
+ // (cache_name+num_shard_bits+ghost_capacity).
+ void WriteMissTimeline(uint64_t time_unit) const;
+
+ // Write the access timeline into a csv file saved in 'output_dir'.
+ //
+ // The file is named "label_access_timeline".The file format is
+ // "time,label_1_access_per_second,label_2_access_per_second,...,label_N_access_per_second"
+ // where N is the number of unique labels found in the trace.
+ void WriteAccessTimeline(const std::string& label, uint64_t time_unit,
+ bool user_access_only) const;
+
+ // Write the reuse distance into a csv file saved in 'output_dir'. Reuse
+ // distance is defined as the cumulated size of unique blocks read between two
+ // consective accesses on the same block.
+ //
+ // The file is named "label_reuse_distance". The file format is
+ // bucket,label_1,label_2,...,label_N.
+ void WriteReuseDistance(const std::string& label_str,
+ const std::vector<uint64_t>& distance_buckets) const;
+
+ // Write the reuse interval into a csv file saved in 'output_dir'. Reuse
+ // interval is defined as the time between two consecutive accesses on the
+ // same block.
+ //
+ // The file is named "label_reuse_interval". The file format is
+ // bucket,label_1,label_2,...,label_N.
+ void WriteReuseInterval(const std::string& label_str,
+ const std::vector<uint64_t>& time_buckets) const;
+
+ // Write the reuse lifetime into a csv file saved in 'output_dir'. Reuse
+ // lifetime is defined as the time interval between the first access of a
+ // block and its last access.
+ //
+ // The file is named "label_reuse_lifetime". The file format is
+ // bucket,label_1,label_2,...,label_N.
+ void WriteReuseLifetime(const std::string& label_str,
+ const std::vector<uint64_t>& time_buckets) const;
+
+ // Write the reuse timeline into a csv file saved in 'output_dir'.
+ //
+ // The file is named
+ // "block_type_user_access_only_reuse_window_reuse_timeline". The file format
+ // is start_time,0,1,...,N where N equals trace_duration / reuse_window.
+ void WriteBlockReuseTimeline(const uint64_t reuse_window,
+ bool user_access_only,
+ TraceType block_type) const;
+
+ // Write the Get spatical locality into csv files saved in 'output_dir'.
+ //
+ // It generates three csv files. label_percent_ref_keys,
+ // label_percent_accesses_on_ref_keys, and
+ // label_percent_data_size_on_ref_keys.
+ void WriteGetSpatialLocality(
+ const std::string& label_str,
+ const std::vector<uint64_t>& percent_buckets) const;
+
+ void WriteCorrelationFeatures(const std::string& label_str,
+ uint32_t max_number_of_values) const;
+
+ void WriteCorrelationFeaturesForGet(uint32_t max_number_of_values) const;
+
+ void WriteSkewness(const std::string& label_str,
+ const std::vector<uint64_t>& percent_buckets,
+ TraceType target_block_type) const;
+
+ const std::map<std::string, ColumnFamilyAccessInfoAggregate>&
+ TEST_cf_aggregates_map() const {
+ return cf_aggregates_map_;
+ }
+
+ private:
+ std::set<std::string> ParseLabelStr(const std::string& label_str) const;
+
+ std::string BuildLabel(const std::set<std::string>& labels,
+ const std::string& cf_name, uint64_t fd,
+ uint32_t level, TraceType type,
+ TableReaderCaller caller, uint64_t block_key,
+ const BlockAccessInfo& block) const;
+
+ void ComputeReuseDistance(BlockAccessInfo* info) const;
+
+ Status RecordAccess(const BlockCacheTraceRecord& access);
+
+ void UpdateReuseIntervalStats(
+ const std::string& label, const std::vector<uint64_t>& time_buckets,
+ const std::map<uint64_t, uint64_t> timeline,
+ std::map<std::string, std::map<uint64_t, uint64_t>>*
+ label_time_num_reuses,
+ uint64_t* total_num_reuses) const;
+
+ std::string OutputPercentAccessStats(
+ uint64_t total_accesses,
+ const std::map<std::string, uint64_t>& cf_access_count) const;
+
+ void WriteStatsToFile(
+ const std::string& label_str, const std::vector<uint64_t>& time_buckets,
+ const std::string& filename_suffix,
+ const std::map<std::string, std::map<uint64_t, uint64_t>>& label_data,
+ uint64_t ntotal) const;
+
+ void TraverseBlocks(
+ std::function<void(const std::string& /*cf_name*/, uint64_t /*fd*/,
+ uint32_t /*level*/, TraceType /*block_type*/,
+ const std::string& /*block_key*/,
+ uint64_t /*block_key_id*/,
+ const BlockAccessInfo& /*block_access_info*/)>
+ block_callback,
+ std::set<std::string>* labels = nullptr) const;
+
+ void UpdateFeatureVectors(
+ const std::vector<uint64_t>& access_sequence_number_timeline,
+ const std::vector<uint64_t>& access_timeline, const std::string& label,
+ std::map<std::string, Features>* label_features,
+ std::map<std::string, Predictions>* label_predictions) const;
+
+ void WriteCorrelationFeaturesToFile(
+ const std::string& label,
+ const std::map<std::string, Features>& label_features,
+ const std::map<std::string, Predictions>& label_predictions,
+ uint32_t max_number_of_values) const;
+
+ ROCKSDB_NAMESPACE::Env* env_;
+ const std::string trace_file_path_;
+ const std::string output_dir_;
+ std::string human_readable_trace_file_path_;
+ const bool compute_reuse_distance_;
+ const bool mrc_only_;
+ const bool is_human_readable_trace_file_;
+
+ BlockCacheTraceHeader header_;
+ std::unique_ptr<BlockCacheTraceSimulator> cache_simulator_;
+ std::map<std::string, ColumnFamilyAccessInfoAggregate> cf_aggregates_map_;
+ std::map<std::string, BlockAccessInfo*> block_info_map_;
+ std::unordered_map<std::string, GetKeyInfo> get_key_info_map_;
+ uint64_t access_sequence_number_ = 0;
+ uint64_t trace_start_timestamp_in_seconds_ = 0;
+ uint64_t trace_end_timestamp_in_seconds_ = 0;
+ MissRatioStats miss_ratio_stats_;
+ uint64_t unique_block_id_ = 1;
+ uint64_t unique_get_key_id_ = 1;
+ BlockCacheHumanReadableTraceWriter human_readable_trace_writer_;
+};
+
+int block_cache_trace_analyzer_tool(int argc, char** argv);
+
+} // namespace ROCKSDB_NAMESPACE
diff --git a/src/rocksdb/tools/block_cache_analyzer/block_cache_trace_analyzer_plot.py b/src/rocksdb/tools/block_cache_analyzer/block_cache_trace_analyzer_plot.py
new file mode 100644
index 000000000..37166bcb4
--- /dev/null
+++ b/src/rocksdb/tools/block_cache_analyzer/block_cache_trace_analyzer_plot.py
@@ -0,0 +1,729 @@
+# Copyright (c) Meta Platforms, Inc. and affiliates.
+#
+# This source code is licensed under both the GPLv2 (found in the
+# COPYING file in the root directory) and Apache 2.0 License
+# (found in the LICENSE.Apache file in the root directory).
+
+#!/usr/bin/env python3
+
+import csv
+import math
+import os
+import random
+import sys
+
+import matplotlib
+
+matplotlib.use("Agg")
+import matplotlib.backends.backend_pdf
+import matplotlib.pyplot as plt
+import numpy as np
+import pandas as pd
+import seaborn as sns
+
+
+# Make sure a legend has the same color across all generated graphs.
+def get_cmap(n, name="hsv"):
+ """Returns a function that maps each index in 0, 1, ..., n-1 to a distinct
+ RGB color; the keyword argument name must be a standard mpl colormap name."""
+ return plt.cm.get_cmap(name, n)
+
+
+color_index = 0
+bar_color_maps = {}
+colors = []
+n_colors = 360
+linear_colors = get_cmap(n_colors)
+for i in range(n_colors):
+ colors.append(linear_colors(i))
+# Shuffle the colors so that adjacent bars in a graph are obvious to differentiate.
+random.shuffle(colors)
+
+
+def num_to_gb(n):
+ one_gb = 1024 * 1024 * 1024
+ if float(n) % one_gb == 0:
+ return "{}".format(n / one_gb)
+ # Keep two decimal points.
+ return "{0:.2f}".format(float(n) / one_gb)
+
+
+def plot_miss_stats_graphs(
+ csv_result_dir, output_result_dir, file_prefix, file_suffix, ylabel, pdf_file_name
+):
+ miss_ratios = {}
+ for file in os.listdir(csv_result_dir):
+ if not file.startswith(file_prefix):
+ continue
+ if not file.endswith(file_suffix):
+ continue
+ print("Processing file {}/{}".format(csv_result_dir, file))
+ mrc_file_path = csv_result_dir + "/" + file
+ with open(mrc_file_path, "r") as csvfile:
+ rows = csv.reader(csvfile, delimiter=",")
+ for row in rows:
+ cache_name = row[0]
+ num_shard_bits = int(row[1])
+ ghost_capacity = int(row[2])
+ capacity = int(row[3])
+ miss_ratio = float(row[4])
+ config = "{}-{}-{}".format(cache_name, num_shard_bits, ghost_capacity)
+ if config not in miss_ratios:
+ miss_ratios[config] = {}
+ miss_ratios[config]["x"] = []
+ miss_ratios[config]["y"] = []
+ miss_ratios[config]["x"].append(capacity)
+ miss_ratios[config]["y"].append(miss_ratio)
+ fig = plt.figure()
+ for config in miss_ratios:
+ plt.plot(
+ miss_ratios[config]["x"], miss_ratios[config]["y"], label=config
+ )
+ plt.xlabel("Cache capacity")
+ plt.ylabel(ylabel)
+ plt.xscale("log", basex=2)
+ plt.ylim(ymin=0)
+ plt.title("{}".format(file))
+ plt.legend()
+ fig.savefig(
+ output_result_dir + "/{}.pdf".format(pdf_file_name), bbox_inches="tight"
+ )
+
+
+def plot_miss_stats_diff_lru_graphs(
+ csv_result_dir, output_result_dir, file_prefix, file_suffix, ylabel, pdf_file_name
+):
+ miss_ratios = {}
+ for file in os.listdir(csv_result_dir):
+ if not file.startswith(file_prefix):
+ continue
+ if not file.endswith(file_suffix):
+ continue
+ print("Processing file {}/{}".format(csv_result_dir, file))
+ mrc_file_path = csv_result_dir + "/" + file
+ with open(mrc_file_path, "r") as csvfile:
+ rows = csv.reader(csvfile, delimiter=",")
+ for row in rows:
+ cache_name = row[0]
+ num_shard_bits = int(row[1])
+ ghost_capacity = int(row[2])
+ capacity = int(row[3])
+ miss_ratio = float(row[4])
+ config = "{}-{}-{}".format(cache_name, num_shard_bits, ghost_capacity)
+ if config not in miss_ratios:
+ miss_ratios[config] = {}
+ miss_ratios[config]["x"] = []
+ miss_ratios[config]["y"] = []
+ miss_ratios[config]["x"].append(capacity)
+ miss_ratios[config]["y"].append(miss_ratio)
+ if "lru-0-0" not in miss_ratios:
+ return
+ fig = plt.figure()
+ for config in miss_ratios:
+ diffs = [0] * len(miss_ratios["lru-0-0"]["x"])
+ for i in range(len(miss_ratios["lru-0-0"]["x"])):
+ for j in range(len(miss_ratios[config]["x"])):
+ if miss_ratios["lru-0-0"]["x"][i] == miss_ratios[config]["x"][j]:
+ diffs[i] = (
+ miss_ratios[config]["y"][j] - miss_ratios["lru-0-0"]["y"][i]
+ )
+ break
+ plt.plot(miss_ratios["lru-0-0"]["x"], diffs, label=config)
+ plt.xlabel("Cache capacity")
+ plt.ylabel(ylabel)
+ plt.xscale("log", basex=2)
+ plt.title("{}".format(file))
+ plt.legend()
+ fig.savefig(
+ output_result_dir + "/{}.pdf".format(pdf_file_name), bbox_inches="tight"
+ )
+
+
+def sanitize(label):
+ # matplotlib cannot plot legends that is prefixed with "_"
+ # so we need to remove them here.
+ index = 0
+ for i in range(len(label)):
+ if label[i] == "_":
+ index += 1
+ else:
+ break
+ data = label[index:]
+ # The value of uint64_max in c++.
+ if "18446744073709551615" in data:
+ return "max"
+ return data
+
+
+# Read the csv file vertically, i.e., group the data by columns.
+def read_data_for_plot_vertical(csvfile):
+ x = []
+ labels = []
+ label_stats = {}
+ csv_rows = csv.reader(csvfile, delimiter=",")
+ data_rows = []
+ for row in csv_rows:
+ data_rows.append(row)
+ # header
+ for i in range(1, len(data_rows[0])):
+ labels.append(sanitize(data_rows[0][i]))
+ label_stats[i - 1] = []
+ for i in range(1, len(data_rows)):
+ for j in range(len(data_rows[i])):
+ if j == 0:
+ x.append(sanitize(data_rows[i][j]))
+ continue
+ label_stats[j - 1].append(float(data_rows[i][j]))
+ return x, labels, label_stats
+
+
+# Read the csv file horizontally, i.e., group the data by rows.
+def read_data_for_plot_horizontal(csvfile):
+ x = []
+ labels = []
+ label_stats = {}
+ csv_rows = csv.reader(csvfile, delimiter=",")
+ data_rows = []
+ for row in csv_rows:
+ data_rows.append(row)
+ # header
+ for i in range(1, len(data_rows)):
+ labels.append(sanitize(data_rows[i][0]))
+ label_stats[i - 1] = []
+ for i in range(1, len(data_rows[0])):
+ x.append(sanitize(data_rows[0][i]))
+ for i in range(1, len(data_rows)):
+ for j in range(len(data_rows[i])):
+ if j == 0:
+ # label
+ continue
+ label_stats[i - 1].append(float(data_rows[i][j]))
+ return x, labels, label_stats
+
+
+def read_data_for_plot(csvfile, vertical):
+ if vertical:
+ return read_data_for_plot_vertical(csvfile)
+ return read_data_for_plot_horizontal(csvfile)
+
+
+def plot_line_charts(
+ csv_result_dir,
+ output_result_dir,
+ filename_prefix,
+ filename_suffix,
+ pdf_name,
+ xlabel,
+ ylabel,
+ title,
+ vertical,
+ legend,
+):
+ global color_index, bar_color_maps, colors
+ pdf = matplotlib.backends.backend_pdf.PdfPages(output_result_dir + "/" + pdf_name)
+ for file in os.listdir(csv_result_dir):
+ if not file.endswith(filename_suffix):
+ continue
+ if not file.startswith(filename_prefix):
+ continue
+ print("Processing file {}/{}".format(csv_result_dir, file))
+ with open(csv_result_dir + "/" + file, "r") as csvfile:
+ x, labels, label_stats = read_data_for_plot(csvfile, vertical)
+ if len(x) == 0 or len(labels) == 0:
+ continue
+ # plot figure
+ fig = plt.figure()
+ for label_index in label_stats:
+ # Assign a unique color to this label.
+ if labels[label_index] not in bar_color_maps:
+ bar_color_maps[labels[label_index]] = colors[color_index]
+ color_index += 1
+ plt.plot(
+ [int(x[i]) for i in range(len(x) - 1)],
+ label_stats[label_index][:-1],
+ label=labels[label_index],
+ color=bar_color_maps[labels[label_index]],
+ )
+
+ # Translate time unit into x labels.
+ if "_60" in file:
+ plt.xlabel("{} (Minute)".format(xlabel))
+ if "_3600" in file:
+ plt.xlabel("{} (Hour)".format(xlabel))
+ plt.ylabel(ylabel)
+ plt.title("{} {}".format(title, file))
+ if legend:
+ plt.legend()
+ pdf.savefig(fig)
+ pdf.close()
+
+
+def plot_stacked_bar_charts(
+ csv_result_dir,
+ output_result_dir,
+ filename_suffix,
+ pdf_name,
+ xlabel,
+ ylabel,
+ title,
+ vertical,
+ x_prefix,
+):
+ global color_index, bar_color_maps, colors
+ pdf = matplotlib.backends.backend_pdf.PdfPages(
+ "{}/{}".format(output_result_dir, pdf_name)
+ )
+ for file in os.listdir(csv_result_dir):
+ if not file.endswith(filename_suffix):
+ continue
+ with open(csv_result_dir + "/" + file, "r") as csvfile:
+ print("Processing file {}/{}".format(csv_result_dir, file))
+ x, labels, label_stats = read_data_for_plot(csvfile, vertical)
+ if len(x) == 0 or len(label_stats) == 0:
+ continue
+ # Plot figure
+ fig = plt.figure()
+ ind = np.arange(len(x)) # the x locations for the groups
+ width = 0.5 # the width of the bars: can also be len(x) sequence
+ bars = []
+ bottom_bars = []
+ for _i in label_stats[0]:
+ bottom_bars.append(0)
+ for i in range(0, len(label_stats)):
+ # Assign a unique color to this label.
+ if labels[i] not in bar_color_maps:
+ bar_color_maps[labels[i]] = colors[color_index]
+ color_index += 1
+ p = plt.bar(
+ ind,
+ label_stats[i],
+ width,
+ bottom=bottom_bars,
+ color=bar_color_maps[labels[i]],
+ )
+ bars.append(p[0])
+ for j in range(len(label_stats[i])):
+ bottom_bars[j] += label_stats[i][j]
+ plt.xlabel(xlabel)
+ plt.ylabel(ylabel)
+ plt.xticks(
+ ind, [x_prefix + x[i] for i in range(len(x))], rotation=20, fontsize=8
+ )
+ plt.legend(bars, labels)
+ plt.title("{} filename:{}".format(title, file))
+ pdf.savefig(fig)
+ pdf.close()
+
+
+def plot_heatmap(csv_result_dir, output_result_dir, filename_suffix, pdf_name, title):
+ pdf = matplotlib.backends.backend_pdf.PdfPages(
+ "{}/{}".format(output_result_dir, pdf_name)
+ )
+ for file in os.listdir(csv_result_dir):
+ if not file.endswith(filename_suffix):
+ continue
+ csv_file_name = "{}/{}".format(csv_result_dir, file)
+ print("Processing file {}/{}".format(csv_result_dir, file))
+ corr_table = pd.read_csv(csv_file_name)
+ corr_table = corr_table.pivot("label", "corr", "value")
+ fig = plt.figure()
+ sns.heatmap(corr_table, annot=True, linewidths=0.5, fmt=".2")
+ plt.title("{} filename:{}".format(title, file))
+ pdf.savefig(fig)
+ pdf.close()
+
+
+def plot_timeline(csv_result_dir, output_result_dir):
+ plot_line_charts(
+ csv_result_dir,
+ output_result_dir,
+ filename_prefix="",
+ filename_suffix="access_timeline",
+ pdf_name="access_time.pdf",
+ xlabel="Time",
+ ylabel="Throughput",
+ title="Access timeline with group by label",
+ vertical=False,
+ legend=True,
+ )
+
+
+def convert_to_0_if_nan(n):
+ if math.isnan(n):
+ return 0.0
+ return n
+
+
+def plot_correlation(csv_result_dir, output_result_dir):
+ # Processing the correlation input first.
+ label_str_file = {}
+ for file in os.listdir(csv_result_dir):
+ if not file.endswith("correlation_input"):
+ continue
+ csv_file_name = "{}/{}".format(csv_result_dir, file)
+ print("Processing file {}/{}".format(csv_result_dir, file))
+ corr_table = pd.read_csv(csv_file_name)
+ label_str = file.split("_")[0]
+ label = file[len(label_str) + 1 :]
+ label = label[: len(label) - len("_correlation_input")]
+
+ output_file = "{}/{}_correlation_output".format(csv_result_dir, label_str)
+ if output_file not in label_str_file:
+ f = open("{}/{}_correlation_output".format(csv_result_dir, label_str), "w+")
+ label_str_file[output_file] = f
+ f.write("label,corr,value\n")
+ f = label_str_file[output_file]
+ f.write(
+ "{},{},{}\n".format(
+ label,
+ "LA+A",
+ convert_to_0_if_nan(
+ corr_table["num_accesses_since_last_access"].corr(
+ corr_table["num_accesses_till_next_access"], method="spearman"
+ )
+ ),
+ )
+ )
+ f.write(
+ "{},{},{}\n".format(
+ label,
+ "PA+A",
+ convert_to_0_if_nan(
+ corr_table["num_past_accesses"].corr(
+ corr_table["num_accesses_till_next_access"], method="spearman"
+ )
+ ),
+ )
+ )
+ f.write(
+ "{},{},{}\n".format(
+ label,
+ "LT+A",
+ convert_to_0_if_nan(
+ corr_table["elapsed_time_since_last_access"].corr(
+ corr_table["num_accesses_till_next_access"], method="spearman"
+ )
+ ),
+ )
+ )
+ f.write(
+ "{},{},{}\n".format(
+ label,
+ "LA+T",
+ convert_to_0_if_nan(
+ corr_table["num_accesses_since_last_access"].corr(
+ corr_table["elapsed_time_till_next_access"], method="spearman"
+ )
+ ),
+ )
+ )
+ f.write(
+ "{},{},{}\n".format(
+ label,
+ "LT+T",
+ convert_to_0_if_nan(
+ corr_table["elapsed_time_since_last_access"].corr(
+ corr_table["elapsed_time_till_next_access"], method="spearman"
+ )
+ ),
+ )
+ )
+ f.write(
+ "{},{},{}\n".format(
+ label,
+ "PA+T",
+ convert_to_0_if_nan(
+ corr_table["num_past_accesses"].corr(
+ corr_table["elapsed_time_till_next_access"], method="spearman"
+ )
+ ),
+ )
+ )
+ for label_str in label_str_file:
+ label_str_file[label_str].close()
+
+ plot_heatmap(
+ csv_result_dir,
+ output_result_dir,
+ "correlation_output",
+ "correlation.pdf",
+ "Correlation",
+ )
+
+
+def plot_reuse_graphs(csv_result_dir, output_result_dir):
+ plot_stacked_bar_charts(
+ csv_result_dir,
+ output_result_dir,
+ filename_suffix="avg_reuse_interval_naccesses",
+ pdf_name="avg_reuse_interval_naccesses.pdf",
+ xlabel="",
+ ylabel="Percentage of accesses",
+ title="Average reuse interval",
+ vertical=True,
+ x_prefix="< ",
+ )
+ plot_stacked_bar_charts(
+ csv_result_dir,
+ output_result_dir,
+ filename_suffix="avg_reuse_interval",
+ pdf_name="avg_reuse_interval.pdf",
+ xlabel="",
+ ylabel="Percentage of blocks",
+ title="Average reuse interval",
+ vertical=True,
+ x_prefix="< ",
+ )
+ plot_stacked_bar_charts(
+ csv_result_dir,
+ output_result_dir,
+ filename_suffix="access_reuse_interval",
+ pdf_name="reuse_interval.pdf",
+ xlabel="Seconds",
+ ylabel="Percentage of accesses",
+ title="Reuse interval",
+ vertical=True,
+ x_prefix="< ",
+ )
+ plot_stacked_bar_charts(
+ csv_result_dir,
+ output_result_dir,
+ filename_suffix="reuse_lifetime",
+ pdf_name="reuse_lifetime.pdf",
+ xlabel="Seconds",
+ ylabel="Percentage of blocks",
+ title="Reuse lifetime",
+ vertical=True,
+ x_prefix="< ",
+ )
+ plot_line_charts(
+ csv_result_dir,
+ output_result_dir,
+ filename_prefix="",
+ filename_suffix="reuse_blocks_timeline",
+ pdf_name="reuse_blocks_timeline.pdf",
+ xlabel="",
+ ylabel="Percentage of blocks",
+ title="Reuse blocks timeline",
+ vertical=False,
+ legend=False,
+ )
+
+
+def plot_percentage_access_summary(csv_result_dir, output_result_dir):
+ plot_stacked_bar_charts(
+ csv_result_dir,
+ output_result_dir,
+ filename_suffix="percentage_of_accesses_summary",
+ pdf_name="percentage_access.pdf",
+ xlabel="",
+ ylabel="Percentage of accesses",
+ title="",
+ vertical=True,
+ x_prefix="",
+ )
+ plot_stacked_bar_charts(
+ csv_result_dir,
+ output_result_dir,
+ filename_suffix="percent_ref_keys",
+ pdf_name="percent_ref_keys.pdf",
+ xlabel="",
+ ylabel="Percentage of blocks",
+ title="",
+ vertical=True,
+ x_prefix="",
+ )
+ plot_stacked_bar_charts(
+ csv_result_dir,
+ output_result_dir,
+ filename_suffix="percent_data_size_on_ref_keys",
+ pdf_name="percent_data_size_on_ref_keys.pdf",
+ xlabel="",
+ ylabel="Percentage of blocks",
+ title="",
+ vertical=True,
+ x_prefix="",
+ )
+ plot_stacked_bar_charts(
+ csv_result_dir,
+ output_result_dir,
+ filename_suffix="percent_accesses_on_ref_keys",
+ pdf_name="percent_accesses_on_ref_keys.pdf",
+ xlabel="",
+ ylabel="Percentage of blocks",
+ title="",
+ vertical=True,
+ x_prefix="",
+ )
+
+
+def plot_access_count_summary(csv_result_dir, output_result_dir):
+ plot_stacked_bar_charts(
+ csv_result_dir,
+ output_result_dir,
+ filename_suffix="access_count_summary",
+ pdf_name="access_count_summary.pdf",
+ xlabel="Access count",
+ ylabel="Percentage of blocks",
+ title="",
+ vertical=True,
+ x_prefix="< ",
+ )
+ plot_line_charts(
+ csv_result_dir,
+ output_result_dir,
+ filename_prefix="",
+ filename_suffix="skewness",
+ pdf_name="skew.pdf",
+ xlabel="",
+ ylabel="Percentage of accesses",
+ title="Skewness",
+ vertical=True,
+ legend=False,
+ )
+
+
+def plot_miss_ratio_timeline(csv_result_dir, output_result_dir):
+ plot_line_charts(
+ csv_result_dir,
+ output_result_dir,
+ filename_prefix="",
+ filename_suffix="3600_miss_ratio_timeline",
+ pdf_name="miss_ratio_timeline.pdf",
+ xlabel="Time",
+ ylabel="Miss Ratio (%)",
+ title="Miss ratio timeline",
+ vertical=False,
+ legend=True,
+ )
+ plot_line_charts(
+ csv_result_dir,
+ output_result_dir,
+ filename_prefix="",
+ filename_suffix="3600_miss_timeline",
+ pdf_name="miss_timeline.pdf",
+ xlabel="Time",
+ ylabel="# of misses ",
+ title="Miss timeline",
+ vertical=False,
+ legend=True,
+ )
+ plot_line_charts(
+ csv_result_dir,
+ output_result_dir,
+ filename_prefix="",
+ filename_suffix="3600_miss_timeline",
+ pdf_name="miss_timeline.pdf",
+ xlabel="Time",
+ ylabel="# of misses ",
+ title="Miss timeline",
+ vertical=False,
+ legend=True,
+ )
+ plot_line_charts(
+ csv_result_dir,
+ output_result_dir,
+ filename_prefix="",
+ filename_suffix="3600_policy_timeline",
+ pdf_name="policy_timeline.pdf",
+ xlabel="Time",
+ ylabel="# of times a policy is selected ",
+ title="Policy timeline",
+ vertical=False,
+ legend=True,
+ )
+ plot_line_charts(
+ csv_result_dir,
+ output_result_dir,
+ filename_prefix="",
+ filename_suffix="3600_policy_ratio_timeline",
+ pdf_name="policy_ratio_timeline.pdf",
+ xlabel="Time",
+ ylabel="Percentage of times a policy is selected ",
+ title="Policy timeline",
+ vertical=False,
+ legend=True,
+ )
+
+
+if __name__ == "__main__":
+ if len(sys.argv) < 3:
+ print(
+ "Must provide two arguments: \n"
+ "1) The directory that saves a list of "
+ "directories which contain block cache trace analyzer result files. \n"
+ "2) the directory to save plotted graphs. \n"
+ )
+ exit(1)
+ csv_result_dir = sys.argv[1]
+ output_result_dir = sys.argv[2]
+ print(
+ "Processing directory {} and save graphs to {}.".format(
+ csv_result_dir, output_result_dir
+ )
+ )
+ for csv_relative_dir in os.listdir(csv_result_dir):
+ csv_abs_dir = csv_result_dir + "/" + csv_relative_dir
+ result_dir = output_result_dir + "/" + csv_relative_dir
+ if not os.path.isdir(csv_abs_dir):
+ print("{} is not a directory".format(csv_abs_dir))
+ continue
+ print("Processing experiment dir: {}".format(csv_relative_dir))
+ if not os.path.exists(result_dir):
+ os.makedirs(result_dir)
+ plot_access_count_summary(csv_abs_dir, result_dir)
+ plot_timeline(csv_abs_dir, result_dir)
+ plot_miss_ratio_timeline(csv_result_dir, output_result_dir)
+ plot_correlation(csv_abs_dir, result_dir)
+ plot_reuse_graphs(csv_abs_dir, result_dir)
+ plot_percentage_access_summary(csv_abs_dir, result_dir)
+ plot_miss_stats_graphs(
+ csv_abs_dir,
+ result_dir,
+ file_prefix="",
+ file_suffix="mrc",
+ ylabel="Miss ratio (%)",
+ pdf_file_name="mrc",
+ )
+ plot_miss_stats_diff_lru_graphs(
+ csv_abs_dir,
+ result_dir,
+ file_prefix="",
+ file_suffix="mrc",
+ ylabel="Miss ratio (%)",
+ pdf_file_name="mrc_diff_lru",
+ )
+ # The following stats are only available in pysim.
+ for time_unit in ["1", "60", "3600"]:
+ plot_miss_stats_graphs(
+ csv_abs_dir,
+ result_dir,
+ file_prefix="ml_{}_".format(time_unit),
+ file_suffix="p95mb",
+ ylabel="p95 number of byte miss per {} seconds".format(time_unit),
+ pdf_file_name="p95mb_per{}_seconds".format(time_unit),
+ )
+ plot_miss_stats_graphs(
+ csv_abs_dir,
+ result_dir,
+ file_prefix="ml_{}_".format(time_unit),
+ file_suffix="avgmb",
+ ylabel="Average number of byte miss per {} seconds".format(time_unit),
+ pdf_file_name="avgmb_per{}_seconds".format(time_unit),
+ )
+ plot_miss_stats_diff_lru_graphs(
+ csv_abs_dir,
+ result_dir,
+ file_prefix="ml_{}_".format(time_unit),
+ file_suffix="p95mb",
+ ylabel="p95 number of byte miss per {} seconds".format(time_unit),
+ pdf_file_name="p95mb_per{}_seconds_diff_lru".format(time_unit),
+ )
+ plot_miss_stats_diff_lru_graphs(
+ csv_abs_dir,
+ result_dir,
+ file_prefix="ml_{}_".format(time_unit),
+ file_suffix="avgmb",
+ ylabel="Average number of byte miss per {} seconds".format(time_unit),
+ pdf_file_name="avgmb_per{}_seconds_diff_lru".format(time_unit),
+ )
diff --git a/src/rocksdb/tools/block_cache_analyzer/block_cache_trace_analyzer_test.cc b/src/rocksdb/tools/block_cache_analyzer/block_cache_trace_analyzer_test.cc
new file mode 100644
index 000000000..c5d9b1452
--- /dev/null
+++ b/src/rocksdb/tools/block_cache_analyzer/block_cache_trace_analyzer_test.cc
@@ -0,0 +1,735 @@
+// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+// This source code is licensed under both the GPLv2 (found in the
+// COPYING file in the root directory) and Apache 2.0 License
+// (found in the LICENSE.Apache file in the root directory).
+
+#ifndef ROCKSDB_LITE
+#ifndef GFLAGS
+#include <cstdio>
+int main() {
+ fprintf(stderr,
+ "Please install gflags to run block_cache_trace_analyzer_test\n");
+ return 0;
+}
+#else
+
+#include <fstream>
+#include <iostream>
+#include <map>
+#include <vector>
+
+#include "rocksdb/db.h"
+#include "rocksdb/env.h"
+#include "rocksdb/status.h"
+#include "rocksdb/trace_reader_writer.h"
+#include "rocksdb/trace_record.h"
+#include "test_util/testharness.h"
+#include "test_util/testutil.h"
+#include "tools/block_cache_analyzer/block_cache_trace_analyzer.h"
+#include "trace_replay/block_cache_tracer.h"
+
+namespace ROCKSDB_NAMESPACE {
+
+namespace {
+const uint64_t kBlockSize = 1024;
+const std::string kBlockKeyPrefix = "test-block-";
+const uint32_t kCFId = 0;
+const uint32_t kLevel = 1;
+const uint64_t kSSTStoringEvenKeys = 100;
+const uint64_t kSSTStoringOddKeys = 101;
+const std::string kRefKeyPrefix = "test-get-";
+const uint64_t kNumKeysInBlock = 1024;
+const int kMaxArgCount = 100;
+const size_t kArgBufferSize = 100000;
+} // namespace
+
+class BlockCacheTracerTest : public testing::Test {
+ public:
+ BlockCacheTracerTest() {
+ test_path_ = test::PerThreadDBPath("block_cache_trace_analyzer_test");
+ env_ = ROCKSDB_NAMESPACE::Env::Default();
+ EXPECT_OK(env_->CreateDir(test_path_));
+ trace_file_path_ = test_path_ + "/block_cache_trace";
+ block_cache_sim_config_path_ = test_path_ + "/block_cache_sim_config";
+ timeline_labels_ =
+ "block,all,cf,sst,level,bt,caller,cf_sst,cf_level,cf_bt,cf_caller";
+ reuse_distance_labels_ =
+ "block,all,cf,sst,level,bt,caller,cf_sst,cf_level,cf_bt,cf_caller";
+ reuse_distance_buckets_ = "1,1K,1M,1G";
+ reuse_interval_labels_ = "block,all,cf,sst,level,bt,cf_sst,cf_level,cf_bt";
+ reuse_interval_buckets_ = "1,10,100,1000";
+ reuse_lifetime_labels_ = "block,all,cf,sst,level,bt,cf_sst,cf_level,cf_bt";
+ reuse_lifetime_buckets_ = "1,10,100,1000";
+ analyzing_callers_ = "Get,Iterator";
+ access_count_buckets_ = "2,3,4,5,10";
+ analyze_get_spatial_locality_labels_ = "all";
+ analyze_get_spatial_locality_buckets_ = "10,20,30,40,50,60,70,80,90,100";
+ }
+
+ ~BlockCacheTracerTest() override {
+ if (getenv("KEEP_DB")) {
+ printf("The trace file is still at %s\n", trace_file_path_.c_str());
+ return;
+ }
+ EXPECT_OK(env_->DeleteFile(trace_file_path_));
+ EXPECT_OK(env_->DeleteDir(test_path_));
+ }
+
+ TableReaderCaller GetCaller(uint32_t key_id) {
+ uint32_t n = key_id % 5;
+ switch (n) {
+ case 0:
+ return TableReaderCaller::kPrefetch;
+ case 1:
+ return TableReaderCaller::kCompaction;
+ case 2:
+ return TableReaderCaller::kUserGet;
+ case 3:
+ return TableReaderCaller::kUserMultiGet;
+ case 4:
+ return TableReaderCaller::kUserIterator;
+ }
+ // This cannot happend.
+ assert(false);
+ return TableReaderCaller::kMaxBlockCacheLookupCaller;
+ }
+
+ void WriteBlockAccess(BlockCacheTraceWriter* writer, uint32_t from_key_id,
+ TraceType block_type, uint32_t nblocks) {
+ assert(writer);
+ for (uint32_t i = 0; i < nblocks; i++) {
+ uint32_t key_id = from_key_id + i;
+ uint64_t timestamp = (key_id + 1) * kMicrosInSecond;
+ BlockCacheTraceRecord record;
+ record.block_type = block_type;
+ record.block_size = kBlockSize + key_id;
+ record.block_key = kBlockKeyPrefix + std::to_string(key_id);
+ record.access_timestamp = timestamp;
+ record.cf_id = kCFId;
+ record.cf_name = kDefaultColumnFamilyName;
+ record.caller = GetCaller(key_id);
+ record.level = kLevel;
+ if (key_id % 2 == 0) {
+ record.sst_fd_number = kSSTStoringEvenKeys;
+ } else {
+ record.sst_fd_number = kSSTStoringOddKeys;
+ }
+ record.is_cache_hit = false;
+ record.no_insert = false;
+ // Provide these fields for all block types.
+ // The writer should only write these fields for data blocks and the
+ // caller is either GET or MGET.
+ record.referenced_key =
+ kRefKeyPrefix + std::to_string(key_id) + std::string(8, 0);
+ record.referenced_key_exist_in_block = true;
+ record.num_keys_in_block = kNumKeysInBlock;
+ ASSERT_OK(writer->WriteBlockAccess(
+ record, record.block_key, record.cf_name, record.referenced_key));
+ }
+ }
+
+ void AssertBlockAccessInfo(
+ uint32_t key_id, TraceType type,
+ const std::map<std::string, BlockAccessInfo>& block_access_info_map) {
+ auto key_id_str = kBlockKeyPrefix + std::to_string(key_id);
+ ASSERT_TRUE(block_access_info_map.find(key_id_str) !=
+ block_access_info_map.end());
+ auto& block_access_info = block_access_info_map.find(key_id_str)->second;
+ ASSERT_EQ(1, block_access_info.num_accesses);
+ ASSERT_EQ(kBlockSize + key_id, block_access_info.block_size);
+ ASSERT_GT(block_access_info.first_access_time, 0);
+ ASSERT_GT(block_access_info.last_access_time, 0);
+ ASSERT_EQ(1, block_access_info.caller_num_access_map.size());
+ TableReaderCaller expected_caller = GetCaller(key_id);
+ ASSERT_TRUE(block_access_info.caller_num_access_map.find(expected_caller) !=
+ block_access_info.caller_num_access_map.end());
+ ASSERT_EQ(
+ 1,
+ block_access_info.caller_num_access_map.find(expected_caller)->second);
+
+ if ((expected_caller == TableReaderCaller::kUserGet ||
+ expected_caller == TableReaderCaller::kUserMultiGet) &&
+ type == TraceType::kBlockTraceDataBlock) {
+ ASSERT_EQ(kNumKeysInBlock, block_access_info.num_keys);
+ ASSERT_EQ(1, block_access_info.key_num_access_map.size());
+ ASSERT_EQ(0, block_access_info.non_exist_key_num_access_map.size());
+ ASSERT_EQ(1, block_access_info.num_referenced_key_exist_in_block);
+ }
+ }
+
+ void RunBlockCacheTraceAnalyzer() {
+ std::vector<std::string> params = {
+ "./block_cache_trace_analyzer",
+ "-block_cache_trace_path=" + trace_file_path_,
+ "-block_cache_sim_config_path=" + block_cache_sim_config_path_,
+ "-block_cache_analysis_result_dir=" + test_path_,
+ "-print_block_size_stats",
+ "-print_access_count_stats",
+ "-print_data_block_access_count_stats",
+ "-cache_sim_warmup_seconds=0",
+ "-analyze_bottom_k_access_count_blocks=5",
+ "-analyze_top_k_access_count_blocks=5",
+ "-analyze_blocks_reuse_k_reuse_window=5",
+ "-timeline_labels=" + timeline_labels_,
+ "-reuse_distance_labels=" + reuse_distance_labels_,
+ "-reuse_distance_buckets=" + reuse_distance_buckets_,
+ "-reuse_interval_labels=" + reuse_interval_labels_,
+ "-reuse_interval_buckets=" + reuse_interval_buckets_,
+ "-reuse_lifetime_labels=" + reuse_lifetime_labels_,
+ "-reuse_lifetime_buckets=" + reuse_lifetime_buckets_,
+ "-analyze_callers=" + analyzing_callers_,
+ "-access_count_buckets=" + access_count_buckets_,
+ "-analyze_get_spatial_locality_labels=" +
+ analyze_get_spatial_locality_labels_,
+ "-analyze_get_spatial_locality_buckets=" +
+ analyze_get_spatial_locality_buckets_,
+ "-analyze_correlation_coefficients_labels=all",
+ "-skew_labels=all",
+ "-skew_buckets=10,50,100"};
+ char arg_buffer[kArgBufferSize];
+ char* argv[kMaxArgCount];
+ int argc = 0;
+ int cursor = 0;
+ for (const auto& arg : params) {
+ ASSERT_LE(cursor + arg.size() + 1, kArgBufferSize);
+ ASSERT_LE(argc + 1, kMaxArgCount);
+ snprintf(arg_buffer + cursor, arg.size() + 1, "%s", arg.c_str());
+
+ argv[argc++] = arg_buffer + cursor;
+ cursor += static_cast<int>(arg.size()) + 1;
+ }
+ ASSERT_EQ(0,
+ ROCKSDB_NAMESPACE::block_cache_trace_analyzer_tool(argc, argv));
+ }
+
+ Env* env_;
+ EnvOptions env_options_;
+ std::string block_cache_sim_config_path_;
+ std::string trace_file_path_;
+ std::string test_path_;
+ std::string timeline_labels_;
+ std::string reuse_distance_labels_;
+ std::string reuse_distance_buckets_;
+ std::string reuse_interval_labels_;
+ std::string reuse_interval_buckets_;
+ std::string reuse_lifetime_labels_;
+ std::string reuse_lifetime_buckets_;
+ std::string analyzing_callers_;
+ std::string access_count_buckets_;
+ std::string analyze_get_spatial_locality_labels_;
+ std::string analyze_get_spatial_locality_buckets_;
+};
+
+TEST_F(BlockCacheTracerTest, BlockCacheAnalyzer) {
+ {
+ // Generate a trace file.
+ BlockCacheTraceWriterOptions trace_writer_opt;
+ std::unique_ptr<TraceWriter> trace_writer;
+ ASSERT_OK(NewFileTraceWriter(env_, env_options_, trace_file_path_,
+ &trace_writer));
+ const auto& clock = env_->GetSystemClock();
+ std::unique_ptr<BlockCacheTraceWriter> block_cache_trace_writer =
+ NewBlockCacheTraceWriter(clock.get(), trace_writer_opt,
+ std::move(trace_writer));
+ ASSERT_NE(block_cache_trace_writer, nullptr);
+ ASSERT_OK(block_cache_trace_writer->WriteHeader());
+ WriteBlockAccess(block_cache_trace_writer.get(), 0,
+ TraceType::kBlockTraceDataBlock, 50);
+ ASSERT_OK(env_->FileExists(trace_file_path_));
+ }
+ {
+ // Generate a cache sim config.
+ std::string config = "lru,1,0,1K,1M,1G";
+ std::ofstream out(block_cache_sim_config_path_);
+ ASSERT_TRUE(out.is_open());
+ out << config << std::endl;
+ out.close();
+ }
+ RunBlockCacheTraceAnalyzer();
+ {
+ // Validate the cache miss ratios.
+ std::vector<uint64_t> expected_capacities{1024, 1024 * 1024,
+ 1024 * 1024 * 1024};
+ const std::string mrc_path = test_path_ + "/49_50_mrc";
+ std::ifstream infile(mrc_path);
+ uint32_t config_index = 0;
+ std::string line;
+ // Read header.
+ ASSERT_TRUE(getline(infile, line));
+ while (getline(infile, line)) {
+ std::stringstream ss(line);
+ std::vector<std::string> result_strs;
+ while (ss.good()) {
+ std::string substr;
+ getline(ss, substr, ',');
+ result_strs.push_back(substr);
+ }
+ ASSERT_EQ(6, result_strs.size());
+ ASSERT_LT(config_index, expected_capacities.size());
+ ASSERT_EQ("lru", result_strs[0]); // cache_name
+ ASSERT_EQ("1", result_strs[1]); // num_shard_bits
+ ASSERT_EQ("0", result_strs[2]); // ghost_cache_capacity
+ ASSERT_EQ(std::to_string(expected_capacities[config_index]),
+ result_strs[3]); // cache_capacity
+ ASSERT_EQ("100.0000", result_strs[4]); // miss_ratio
+ ASSERT_EQ("50", result_strs[5]); // number of accesses.
+ config_index++;
+ }
+ ASSERT_EQ(expected_capacities.size(), config_index);
+ infile.close();
+ ASSERT_OK(env_->DeleteFile(mrc_path));
+
+ const std::vector<std::string> time_units{"1", "60", "3600"};
+ expected_capacities.push_back(std::numeric_limits<uint64_t>::max());
+ for (auto const& expected_capacity : expected_capacities) {
+ for (auto const& time_unit : time_units) {
+ const std::string miss_ratio_timeline_path =
+ test_path_ + "/" + std::to_string(expected_capacity) + "_" +
+ time_unit + "_miss_ratio_timeline";
+ std::ifstream mrt_file(miss_ratio_timeline_path);
+ // Read header.
+ ASSERT_TRUE(getline(mrt_file, line));
+ ASSERT_TRUE(getline(mrt_file, line));
+ std::stringstream ss(line);
+ bool read_header = false;
+ while (ss.good()) {
+ std::string substr;
+ getline(ss, substr, ',');
+ if (!read_header) {
+ if (expected_capacity == std::numeric_limits<uint64_t>::max()) {
+ ASSERT_EQ("trace", substr);
+ } else {
+ ASSERT_EQ("lru-1-0", substr);
+ }
+ read_header = true;
+ continue;
+ }
+ ASSERT_DOUBLE_EQ(100.0, ParseDouble(substr));
+ }
+ ASSERT_FALSE(getline(mrt_file, line));
+ mrt_file.close();
+ ASSERT_OK(env_->DeleteFile(miss_ratio_timeline_path));
+ }
+ for (auto const& time_unit : time_units) {
+ const std::string miss_timeline_path =
+ test_path_ + "/" + std::to_string(expected_capacity) + "_" +
+ time_unit + "_miss_timeline";
+ std::ifstream mt_file(miss_timeline_path);
+ // Read header.
+ ASSERT_TRUE(getline(mt_file, line));
+ ASSERT_TRUE(getline(mt_file, line));
+ std::stringstream ss(line);
+ uint32_t num_misses = 0;
+ while (ss.good()) {
+ std::string substr;
+ getline(ss, substr, ',');
+ if (num_misses == 0) {
+ if (expected_capacity == std::numeric_limits<uint64_t>::max()) {
+ ASSERT_EQ("trace", substr);
+ } else {
+ ASSERT_EQ("lru-1-0", substr);
+ }
+ num_misses++;
+ continue;
+ }
+ num_misses += ParseInt(substr);
+ }
+ ASSERT_EQ(51u, num_misses);
+ ASSERT_FALSE(getline(mt_file, line));
+ mt_file.close();
+ ASSERT_OK(env_->DeleteFile(miss_timeline_path));
+ }
+ }
+ }
+ {
+ // Validate the skewness csv file.
+ const std::string skewness_file_path = test_path_ + "/all_skewness";
+ std::ifstream skew_file(skewness_file_path);
+ // Read header.
+ std::string line;
+ ASSERT_TRUE(getline(skew_file, line));
+ std::stringstream ss(line);
+ double sum_percent = 0;
+ while (getline(skew_file, line)) {
+ std::stringstream ss_naccess(line);
+ std::string substr;
+ bool read_label = false;
+ while (ss_naccess.good()) {
+ ASSERT_TRUE(getline(ss_naccess, substr, ','));
+ if (!read_label) {
+ read_label = true;
+ continue;
+ }
+ sum_percent += ParseDouble(substr);
+ }
+ }
+ ASSERT_EQ(100.0, sum_percent);
+ ASSERT_FALSE(getline(skew_file, line));
+ skew_file.close();
+ ASSERT_OK(env_->DeleteFile(skewness_file_path));
+ }
+ {
+ // Validate the timeline csv files.
+ const std::vector<std::string> time_units{"_60", "_3600"};
+ const std::vector<std::string> user_access_only_flags{"user_access_only_",
+ "all_access_"};
+ for (auto const& user_access_only : user_access_only_flags) {
+ for (auto const& unit : time_units) {
+ std::stringstream ss(timeline_labels_);
+ while (ss.good()) {
+ std::string l;
+ ASSERT_TRUE(getline(ss, l, ','));
+ if (l.find("block") == std::string::npos) {
+ if (user_access_only != "all_access_") {
+ continue;
+ }
+ }
+ const std::string timeline_file = test_path_ + "/" +
+ user_access_only + l + unit +
+ "_access_timeline";
+ std::ifstream infile(timeline_file);
+ std::string line;
+ const uint64_t expected_naccesses = 50;
+ const uint64_t expected_user_accesses = 30;
+ ASSERT_TRUE(getline(infile, line)) << timeline_file;
+ uint32_t naccesses = 0;
+ while (getline(infile, line)) {
+ std::stringstream ss_naccess(line);
+ std::string substr;
+ bool read_label = false;
+ while (ss_naccess.good()) {
+ ASSERT_TRUE(getline(ss_naccess, substr, ','));
+ if (!read_label) {
+ read_label = true;
+ continue;
+ }
+ naccesses += ParseUint32(substr);
+ }
+ }
+ if (user_access_only == "user_access_only_") {
+ ASSERT_EQ(expected_user_accesses, naccesses) << timeline_file;
+ } else {
+ ASSERT_EQ(expected_naccesses, naccesses) << timeline_file;
+ }
+ ASSERT_OK(env_->DeleteFile(timeline_file));
+ }
+ }
+ }
+ }
+ {
+ // Validate the reuse_interval and reuse_distance csv files.
+ std::map<std::string, std::string> test_reuse_csv_files;
+ test_reuse_csv_files["_access_reuse_interval"] = reuse_interval_labels_;
+ test_reuse_csv_files["_reuse_distance"] = reuse_distance_labels_;
+ test_reuse_csv_files["_reuse_lifetime"] = reuse_lifetime_labels_;
+ test_reuse_csv_files["_avg_reuse_interval"] = reuse_interval_labels_;
+ test_reuse_csv_files["_avg_reuse_interval_naccesses"] =
+ reuse_interval_labels_;
+ for (auto const& test : test_reuse_csv_files) {
+ const std::string& file_suffix = test.first;
+ const std::string& labels = test.second;
+ const uint32_t expected_num_rows = 5;
+ std::stringstream ss(labels);
+ while (ss.good()) {
+ std::string l;
+ ASSERT_TRUE(getline(ss, l, ','));
+ const std::string reuse_csv_file = test_path_ + "/" + l + file_suffix;
+ std::ifstream infile(reuse_csv_file);
+ std::string line;
+ ASSERT_TRUE(getline(infile, line));
+ double npercentage = 0;
+ uint32_t nrows = 0;
+ while (getline(infile, line)) {
+ std::stringstream ss_naccess(line);
+ bool label_read = false;
+ nrows++;
+ while (ss_naccess.good()) {
+ std::string substr;
+ ASSERT_TRUE(getline(ss_naccess, substr, ','));
+ if (!label_read) {
+ label_read = true;
+ continue;
+ }
+ npercentage += ParseDouble(substr);
+ }
+ }
+ ASSERT_EQ(expected_num_rows, nrows);
+ if ("_reuse_lifetime" == test.first ||
+ "_avg_reuse_interval" == test.first ||
+ "_avg_reuse_interval_naccesses" == test.first) {
+ ASSERT_EQ(100, npercentage) << reuse_csv_file;
+ } else {
+ ASSERT_LT(npercentage, 0);
+ }
+ ASSERT_OK(env_->DeleteFile(reuse_csv_file));
+ }
+ }
+ }
+
+ {
+ // Validate the percentage of accesses summary.
+ const std::string percent_access_summary_file =
+ test_path_ + "/percentage_of_accesses_summary";
+ std::ifstream infile(percent_access_summary_file);
+ std::string line;
+ ASSERT_TRUE(getline(infile, line));
+ std::set<std::string> callers;
+ std::set<std::string> expected_callers{"Get", "MultiGet", "Iterator",
+ "Prefetch", "Compaction"};
+ while (getline(infile, line)) {
+ std::stringstream caller_percent(line);
+ std::string caller;
+ ASSERT_TRUE(getline(caller_percent, caller, ','));
+ std::string percent;
+ ASSERT_TRUE(getline(caller_percent, percent, ','));
+ ASSERT_FALSE(caller_percent.good());
+ callers.insert(caller);
+ ASSERT_EQ(20, ParseDouble(percent));
+ }
+ ASSERT_EQ(expected_callers.size(), callers.size());
+ for (auto caller : callers) {
+ ASSERT_TRUE(expected_callers.find(caller) != expected_callers.end());
+ }
+ ASSERT_OK(env_->DeleteFile(percent_access_summary_file));
+ }
+ {
+ // Validate the percentage of accesses summary by analyzing callers.
+ std::stringstream analyzing_callers(analyzing_callers_);
+ while (analyzing_callers.good()) {
+ std::string caller;
+ ASSERT_TRUE(getline(analyzing_callers, caller, ','));
+ std::vector<std::string> breakdowns{"level", "bt"};
+ for (auto breakdown : breakdowns) {
+ const std::string file_name = test_path_ + "/" + caller + "_" +
+ breakdown +
+ "_percentage_of_accesses_summary";
+ std::ifstream infile(file_name);
+ std::string line;
+ ASSERT_TRUE(getline(infile, line));
+ double sum = 0;
+ while (getline(infile, line)) {
+ std::stringstream label_percent(line);
+ std::string label;
+ ASSERT_TRUE(getline(label_percent, label, ','));
+ std::string percent;
+ ASSERT_TRUE(getline(label_percent, percent, ','));
+ ASSERT_FALSE(label_percent.good());
+ sum += ParseDouble(percent);
+ }
+ ASSERT_EQ(100, sum);
+ ASSERT_OK(env_->DeleteFile(file_name));
+ }
+ }
+ }
+ const std::vector<std::string> access_types{"user_access_only", "all_access"};
+ const std::vector<std::string> prefix{"bt", "cf"};
+ for (auto const& pre : prefix) {
+ for (auto const& access_type : access_types) {
+ {
+ // Validate the access count summary.
+ const std::string bt_access_count_summary = test_path_ + "/" + pre +
+ "_" + access_type +
+ "_access_count_summary";
+ std::ifstream infile(bt_access_count_summary);
+ std::string line;
+ ASSERT_TRUE(getline(infile, line));
+ double sum_percent = 0;
+ while (getline(infile, line)) {
+ std::stringstream bt_percent(line);
+ std::string bt;
+ ASSERT_TRUE(getline(bt_percent, bt, ','));
+ std::string percent;
+ ASSERT_TRUE(getline(bt_percent, percent, ','));
+ sum_percent += ParseDouble(percent);
+ }
+ ASSERT_EQ(100.0, sum_percent);
+ ASSERT_OK(env_->DeleteFile(bt_access_count_summary));
+ }
+ }
+ }
+ for (auto const& access_type : access_types) {
+ std::vector<std::string> block_types{"Index", "Data", "Filter"};
+ for (auto block_type : block_types) {
+ // Validate reuse block timeline.
+ const std::string reuse_blocks_timeline = test_path_ + "/" + block_type +
+ "_" + access_type +
+ "_5_reuse_blocks_timeline";
+ std::ifstream infile(reuse_blocks_timeline);
+ std::string line;
+ ASSERT_TRUE(getline(infile, line)) << reuse_blocks_timeline;
+ uint32_t index = 0;
+ while (getline(infile, line)) {
+ std::stringstream timeline(line);
+ bool start_time = false;
+ double sum = 0;
+ while (timeline.good()) {
+ std::string value;
+ ASSERT_TRUE(getline(timeline, value, ','));
+ if (!start_time) {
+ start_time = true;
+ continue;
+ }
+ sum += ParseDouble(value);
+ }
+ index++;
+ ASSERT_LT(sum, 100.0 * index + 1) << reuse_blocks_timeline;
+ }
+ ASSERT_OK(env_->DeleteFile(reuse_blocks_timeline));
+ }
+ }
+
+ std::stringstream ss(analyze_get_spatial_locality_labels_);
+ while (ss.good()) {
+ std::string l;
+ ASSERT_TRUE(getline(ss, l, ','));
+ const std::vector<std::string> spatial_locality_files{
+ "_percent_ref_keys", "_percent_accesses_on_ref_keys",
+ "_percent_data_size_on_ref_keys"};
+ for (auto const& spatial_locality_file : spatial_locality_files) {
+ const std::string filename = test_path_ + "/" + l + spatial_locality_file;
+ std::ifstream infile(filename);
+ std::string line;
+ ASSERT_TRUE(getline(infile, line));
+ double sum_percent = 0;
+ uint32_t nrows = 0;
+ while (getline(infile, line)) {
+ std::stringstream bt_percent(line);
+ std::string bt;
+ ASSERT_TRUE(getline(bt_percent, bt, ','));
+ std::string percent;
+ ASSERT_TRUE(getline(bt_percent, percent, ','));
+ sum_percent += ParseDouble(percent);
+ nrows++;
+ }
+ ASSERT_EQ(11u, nrows);
+ ASSERT_EQ(100.0, sum_percent);
+ ASSERT_OK(env_->DeleteFile(filename));
+ }
+ }
+ ASSERT_OK(env_->DeleteFile(block_cache_sim_config_path_));
+}
+
+TEST_F(BlockCacheTracerTest, MixedBlocks) {
+ {
+ // Generate a trace file containing a mix of blocks.
+ // It contains two SST files with 25 blocks of odd numbered block_key in
+ // kSSTStoringOddKeys and 25 blocks of even numbered blocks_key in
+ // kSSTStoringEvenKeys.
+ BlockCacheTraceWriterOptions trace_writer_opt;
+ std::unique_ptr<TraceWriter> trace_writer;
+ const auto& clock = env_->GetSystemClock();
+ ASSERT_OK(NewFileTraceWriter(env_, env_options_, trace_file_path_,
+ &trace_writer));
+ std::unique_ptr<BlockCacheTraceWriter> block_cache_trace_writer =
+ NewBlockCacheTraceWriter(clock.get(), trace_writer_opt,
+ std::move(trace_writer));
+ ASSERT_NE(block_cache_trace_writer, nullptr);
+ ASSERT_OK(block_cache_trace_writer->WriteHeader());
+ // Write blocks of different types.
+ WriteBlockAccess(block_cache_trace_writer.get(), 0,
+ TraceType::kBlockTraceUncompressionDictBlock, 10);
+ WriteBlockAccess(block_cache_trace_writer.get(), 10,
+ TraceType::kBlockTraceDataBlock, 10);
+ WriteBlockAccess(block_cache_trace_writer.get(), 20,
+ TraceType::kBlockTraceFilterBlock, 10);
+ WriteBlockAccess(block_cache_trace_writer.get(), 30,
+ TraceType::kBlockTraceIndexBlock, 10);
+ WriteBlockAccess(block_cache_trace_writer.get(), 40,
+ TraceType::kBlockTraceRangeDeletionBlock, 10);
+ ASSERT_OK(env_->FileExists(trace_file_path_));
+ }
+
+ {
+ // Verify trace file is generated correctly.
+ std::unique_ptr<TraceReader> trace_reader;
+ ASSERT_OK(NewFileTraceReader(env_, env_options_, trace_file_path_,
+ &trace_reader));
+ BlockCacheTraceReader reader(std::move(trace_reader));
+ BlockCacheTraceHeader header;
+ ASSERT_OK(reader.ReadHeader(&header));
+ ASSERT_EQ(static_cast<uint32_t>(kMajorVersion),
+ header.rocksdb_major_version);
+ ASSERT_EQ(static_cast<uint32_t>(kMinorVersion),
+ header.rocksdb_minor_version);
+ // Read blocks.
+ BlockCacheTraceAnalyzer analyzer(
+ trace_file_path_,
+ /*output_miss_ratio_curve_path=*/"",
+ /*human_readable_trace_file_path=*/"",
+ /*compute_reuse_distance=*/true,
+ /*mrc_only=*/false,
+ /*is_block_cache_human_readable_trace=*/false,
+ /*simulator=*/nullptr);
+ // The analyzer ends when it detects an incomplete access record.
+ ASSERT_EQ(Status::Incomplete(""), analyzer.Analyze());
+ const uint64_t expected_num_cfs = 1;
+ std::vector<uint64_t> expected_fds{kSSTStoringOddKeys, kSSTStoringEvenKeys};
+ const std::vector<TraceType> expected_types{
+ TraceType::kBlockTraceUncompressionDictBlock,
+ TraceType::kBlockTraceDataBlock, TraceType::kBlockTraceFilterBlock,
+ TraceType::kBlockTraceIndexBlock,
+ TraceType::kBlockTraceRangeDeletionBlock};
+ const uint64_t expected_num_keys_per_type = 5;
+
+ auto& stats = analyzer.TEST_cf_aggregates_map();
+ ASSERT_EQ(expected_num_cfs, stats.size());
+ ASSERT_TRUE(stats.find(kDefaultColumnFamilyName) != stats.end());
+ auto& cf_stats = stats.find(kDefaultColumnFamilyName)->second;
+ ASSERT_EQ(expected_fds.size(), cf_stats.fd_aggregates_map.size());
+ for (auto fd_id : expected_fds) {
+ ASSERT_TRUE(cf_stats.fd_aggregates_map.find(fd_id) !=
+ cf_stats.fd_aggregates_map.end());
+ ASSERT_EQ(kLevel, cf_stats.fd_aggregates_map.find(fd_id)->second.level);
+ auto& block_type_aggregates_map = cf_stats.fd_aggregates_map.find(fd_id)
+ ->second.block_type_aggregates_map;
+ ASSERT_EQ(expected_types.size(), block_type_aggregates_map.size());
+ uint32_t key_id = 0;
+ for (auto type : expected_types) {
+ ASSERT_TRUE(block_type_aggregates_map.find(type) !=
+ block_type_aggregates_map.end());
+ auto& block_access_info_map =
+ block_type_aggregates_map.find(type)->second.block_access_info_map;
+ // Each block type has 5 blocks.
+ ASSERT_EQ(expected_num_keys_per_type, block_access_info_map.size());
+ for (uint32_t i = 0; i < 10; i++) {
+ // Verify that odd numbered blocks are stored in kSSTStoringOddKeys
+ // and even numbered blocks are stored in kSSTStoringEvenKeys.
+ auto key_id_str = kBlockKeyPrefix + std::to_string(key_id);
+ if (fd_id == kSSTStoringOddKeys) {
+ if (key_id % 2 == 1) {
+ AssertBlockAccessInfo(key_id, type, block_access_info_map);
+ } else {
+ ASSERT_TRUE(block_access_info_map.find(key_id_str) ==
+ block_access_info_map.end());
+ }
+ } else {
+ if (key_id % 2 == 1) {
+ ASSERT_TRUE(block_access_info_map.find(key_id_str) ==
+ block_access_info_map.end());
+ } else {
+ AssertBlockAccessInfo(key_id, type, block_access_info_map);
+ }
+ }
+ key_id++;
+ }
+ }
+ }
+ }
+}
+
+} // namespace ROCKSDB_NAMESPACE
+
+int main(int argc, char** argv) {
+ ROCKSDB_NAMESPACE::port::InstallStackTraceHandler();
+ ::testing::InitGoogleTest(&argc, argv);
+ return RUN_ALL_TESTS();
+}
+#endif // GFLAG
+#else
+#include <stdio.h>
+int main(int /*argc*/, char** /*argv*/) {
+ fprintf(stderr,
+ "block_cache_trace_analyzer_test is not supported in ROCKSDB_LITE\n");
+ return 0;
+}
+#endif // ROCKSDB_LITE
diff --git a/src/rocksdb/tools/block_cache_analyzer/block_cache_trace_analyzer_tool.cc b/src/rocksdb/tools/block_cache_analyzer/block_cache_trace_analyzer_tool.cc
new file mode 100644
index 000000000..44fec5598
--- /dev/null
+++ b/src/rocksdb/tools/block_cache_analyzer/block_cache_trace_analyzer_tool.cc
@@ -0,0 +1,25 @@
+// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+// This source code is licensed under both the GPLv2 (found in the
+// COPYING file in the root directory) and Apache 2.0 License
+// (found in the LICENSE.Apache file in the root directory).
+//
+#ifndef ROCKSDB_LITE
+#ifndef GFLAGS
+#include <cstdio>
+int main() {
+ fprintf(stderr, "Please install gflags to run rocksdb tools\n");
+ return 1;
+}
+#else // GFLAGS
+#include "tools/block_cache_analyzer/block_cache_trace_analyzer.h"
+int main(int argc, char** argv) {
+ return ROCKSDB_NAMESPACE::block_cache_trace_analyzer_tool(argc, argv);
+}
+#endif // GFLAGS
+#else // ROCKSDB_LITE
+#include <stdio.h>
+int main(int /*argc*/, char** /*argv*/) {
+ fprintf(stderr, "Not supported in lite mode.\n");
+ return 1;
+}
+#endif // ROCKSDB_LITE
diff --git a/src/rocksdb/tools/check_all_python.py b/src/rocksdb/tools/check_all_python.py
new file mode 100755
index 000000000..708339a67
--- /dev/null
+++ b/src/rocksdb/tools/check_all_python.py
@@ -0,0 +1,22 @@
+#!/usr/bin/env python3
+# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
+import glob
+
+# Checks that all python files in the repository are at least free of syntax
+# errors. This provides a minimal pre-/post-commit check for python file
+# modifications.
+
+filenames = []
+# Avoid scanning all of ./ because there might be other external repos
+# linked in.
+for base in ["buckifier", "build_tools", "coverage", "tools"]:
+ # Clean this up when we finally upgrade to Python 3
+ for suff in ["*", "*/*", "*/*/*"]:
+ filenames += glob.glob(base + "/" + suff + ".py")
+
+for filename in filenames:
+ source = open(filename, "r").read() + "\n"
+ # Parses and syntax checks the file, throwing on error. (No pyc written.)
+ _ = compile(source, filename, "exec")
+
+print("No syntax errors in {0} .py files".format(len(filenames)))
diff --git a/src/rocksdb/tools/check_format_compatible.sh b/src/rocksdb/tools/check_format_compatible.sh
new file mode 100755
index 000000000..8a3f1b379
--- /dev/null
+++ b/src/rocksdb/tools/check_format_compatible.sh
@@ -0,0 +1,379 @@
+#!/usr/bin/env bash
+# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
+#
+# A shell script to build and run different versions of ldb to check for
+# expected forward and backward compatibility with "current" version. The
+# working copy must have no uncommitted changes.
+#
+# Usage: <SCRIPT> [ref_for_current]
+# `ref_for_current` can be a revision, tag, commit or branch name. Default is HEAD.
+#
+# Return value 0 means all regression tests pass. 1 if not pass.
+#
+# Environment options:
+# SHORT_TEST=1 - Test only the oldest branch for each kind of test. This is
+# a good choice for PR validation as it is relatively fast and will find
+# most issues.
+# USE_SSH=1 - Connect to GitHub with ssh instead of https
+
+if ! git diff-index --quiet HEAD; then
+ echo "You have uncommitted changes. Aborting."
+ exit 1
+fi
+
+current_checkout_name=${1:-HEAD}
+# This allows the script to work even if with transient refs like "HEAD"
+current_checkout_hash="$(git rev-parse --quiet --verify $current_checkout_name)"
+
+if [ "$current_checkout_hash" == "" ]; then
+ echo "Not a recognized ref: $current_checkout_name"
+ exit 1
+fi
+
+# To restore to prior branch at the end
+orig_branch="$(git rev-parse --abbrev-ref HEAD)"
+tmp_branch=_tmp_format_compatible
+tmp_origin=_tmp_origin
+
+# Don't depend on what current "origin" might be
+set -e
+git remote remove $tmp_origin 2>/dev/null || true
+if [ "$USE_SSH" ]; then
+ git remote add $tmp_origin "git@github.com:facebook/rocksdb.git"
+else
+ git remote add $tmp_origin "https://github.com/facebook/rocksdb.git"
+fi
+git fetch $tmp_origin
+
+# Used in building some ancient RocksDB versions where by default it tries to
+# use a precompiled libsnappy.a checked in to the repo.
+export SNAPPY_LDFLAGS=-lsnappy
+
+cleanup() {
+ echo "== Cleaning up"
+ git reset --hard || true
+ git checkout "$orig_branch" || true
+ git branch -D $tmp_branch || true
+ git remote remove $tmp_origin || true
+}
+trap cleanup EXIT # Always clean up, even on failure or Ctrl+C
+
+scriptpath=`dirname ${BASH_SOURCE[0]}`
+
+test_dir=${TEST_TMPDIR:-"/tmp"}"/rocksdb_format_compatible_$USER"
+rm -rf ${test_dir:?}
+
+# Prevent 'make clean' etc. from wiping out test_dir
+export TEST_TMPDIR=$test_dir"/misc"
+
+# For saving current version of scripts as we checkout different versions to test
+script_copy_dir=$test_dir"/script_copy"
+mkdir -p $script_copy_dir
+cp -f $scriptpath/*.sh $script_copy_dir
+
+# For shared raw input data
+input_data_path=$test_dir"/test_data_input"
+mkdir -p $input_data_path
+# For external sst ingestion test
+ext_test_dir=$test_dir"/ext"
+mkdir -p $ext_test_dir
+# For DB dump test
+db_test_dir=$test_dir"/db"
+mkdir -p $db_test_dir
+# For backup/restore test (uses DB test)
+bak_test_dir=$test_dir"/bak"
+mkdir -p $bak_test_dir
+
+python_bin=$(which python3 || which python || echo python3)
+
+# Generate random files.
+for i in {1..6}
+do
+ input_data[$i]=$input_data_path/data$i
+ echo == Generating random input file ${input_data[$i]}
+ $python_bin - <<EOF
+import random
+random.seed($i)
+symbols=['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9']
+with open('${input_data[$i]}', 'w') as f:
+ for i in range(1,1024):
+ k = ""
+ for j in range(1, random.randint(1,32)):
+ k=k + symbols[random.randint(0, len(symbols) - 1)]
+ vb = ""
+ for j in range(1, random.randint(0,128)):
+ vb = vb + symbols[random.randint(0, len(symbols) - 1)]
+ v = ""
+ for j in range(1, random.randint(1, 5)):
+ v = v + vb
+ print(k + " ==> " + v, file=f)
+EOF
+done
+
+# Generate file(s) with sorted keys.
+sorted_input_data=$input_data_path/sorted_data
+echo == Generating file with sorted keys ${sorted_input_data}
+$python_bin - <<EOF
+with open('${sorted_input_data}', 'w') as f:
+ for i in range(0,10):
+ k = str(i)
+ v = "value" + k
+ print(k + " ==> " + v, file=f)
+EOF
+
+# db_backward_only_refs defined below the rest
+
+# To check for DB forward compatibility with loading options (old version
+# reading data from new), as well as backward compatibility
+declare -a db_forward_with_options_refs=("6.27.fb" "6.28.fb" "6.29.fb" "7.0.fb" "7.1.fb" "7.2.fb" "7.3.fb" "7.4.fb" "7.5.fb" "7.6.fb" "7.7.fb" "7.8.fb")
+# To check for DB forward compatibility without loading options (in addition
+# to the "with loading options" set), as well as backward compatibility
+declare -a db_forward_no_options_refs=() # N/A at the moment
+
+# To check for SST ingestion backward compatibility (new version reading
+# data from old) (ldb ingest_extern_sst added in 5.16.x, back-ported to
+# 5.14.x, 5.15.x)
+declare -a ext_backward_only_refs=("5.14.fb" "5.15.fb" "5.16.fb" "5.17.fb" "5.18.fb" "6.0.fb" "6.1.fb" "6.2.fb" "6.3.fb" "6.4.fb" "6.5.fb" "6.6.fb" "6.7.fb" "6.8.fb" "6.9.fb" "6.10.fb" "6.11.fb" "6.12.fb" "6.13.fb" "6.14.fb" "6.15.fb" "6.16.fb" "6.17.fb" "6.18.fb" "6.19.fb" "6.20.fb" "6.21.fb" "6.22.fb" "6.23.fb" "6.24.fb" "6.25.fb" "6.26.fb")
+# To check for SST ingestion forward compatibility (old version reading
+# data from new) as well as backward compatibility
+declare -a ext_forward_refs=("${db_forward_no_options_refs[@]}" "${db_forward_with_options_refs[@]}")
+
+# To check for backup backward compatibility (new version reading data
+# from old) (ldb backup/restore added in 4.11.x)
+declare -a bak_backward_only_refs=("4.11.fb" "4.12.fb" "4.13.fb" "5.0.fb" "5.1.fb" "5.2.fb" "5.3.fb" "5.4.fb" "5.5.fb" "5.6.fb" "5.7.fb" "5.8.fb" "5.9.fb" "5.10.fb" "5.11.fb" "5.12.fb" "5.13.fb" "${ext_backward_only_refs[@]}")
+# To check for backup forward compatibility (old version reading data
+# from new) as well as backward compatibility
+declare -a bak_forward_refs=("${db_forward_no_options_refs[@]}" "${db_forward_with_options_refs[@]}")
+
+# Branches (git refs) to check for DB backward compatibility (new version
+# reading data from old) (in addition to the "forward compatible" list)
+# NOTE: 2.7.fb.branch shows assertion violation in some configurations
+declare -a db_backward_only_refs=("2.2.fb.branch" "2.3.fb.branch" "2.4.fb.branch" "2.5.fb.branch" "2.6.fb.branch" "2.7.fb.branch" "2.8.1.fb" "3.0.fb.branch" "3.1.fb" "3.2.fb" "3.3.fb" "3.4.fb" "3.5.fb" "3.6.fb" "3.7.fb" "3.8.fb" "3.9.fb" "4.2.fb" "4.3.fb" "4.4.fb" "4.5.fb" "4.6.fb" "4.7.fb" "4.8.fb" "4.9.fb" "4.10.fb" "${bak_backward_only_refs[@]}")
+
+if [ "$SHORT_TEST" ]; then
+ # Use only the first (if exists) of each list
+ db_backward_only_refs=(${db_backward_only_refs[0]})
+ db_forward_no_options_refs=(${db_forward_no_options_refs[0]})
+ db_forward_with_options_refs=(${db_forward_with_options_refs[0]})
+ ext_backward_only_refs=(${ext_backward_only_refs[0]})
+ ext_forward_refs=(${ext_forward_refs[0]})
+ bak_backward_only_refs=(${bak_backward_only_refs[0]})
+ bak_forward_refs=(${bak_forward_refs[0]})
+fi
+
+# De-duplicate & accumulate
+declare -a checkout_refs=()
+for checkout_ref in "${db_backward_only_refs[@]}" "${db_forward_no_options_refs[@]}" "${db_forward_with_options_refs[@]}" "${ext_backward_only_refs[@]}" "${ext_forward_refs[@]}" "${bak_backward_only_refs[@]}" "${bak_forward_refs[@]}"
+do
+ if [ ! -e $db_test_dir/$checkout_ref ]; then
+ mkdir -p $db_test_dir/$checkout_ref
+ checkout_refs+=($checkout_ref)
+ fi
+done
+
+generate_db()
+{
+ set +e
+ bash "$script_copy_dir"/generate_random_db.sh "$1" "$2"
+ if [ $? -ne 0 ]; then
+ echo ==== Error loading data from $2 to $1 ====
+ exit 1
+ fi
+ set -e
+}
+
+compare_db()
+{
+ set +e
+ bash "$script_copy_dir"/verify_random_db.sh "$1" "$2" "$3" "$4" "$5"
+ if [ $? -ne 0 ]; then
+ echo ==== Read different content from $1 and $2 or error happened. ====
+ exit 1
+ fi
+ set -e
+}
+
+write_external_sst()
+{
+ set +e
+ bash "$script_copy_dir"/write_external_sst.sh "$1" "$2" "$3"
+ if [ $? -ne 0 ]; then
+ echo ==== Error writing external SST file using data from $1 to $3 ====
+ exit 1
+ fi
+ set -e
+}
+
+ingest_external_sst()
+{
+ set +e
+ bash "$script_copy_dir"/ingest_external_sst.sh "$1" "$2"
+ if [ $? -ne 0 ]; then
+ echo ==== Error ingesting external SST in $2 to DB at $1 ====
+ exit 1
+ fi
+ set -e
+}
+
+backup_db()
+{
+ set +e
+ bash "$script_copy_dir"/backup_db.sh "$1" "$2"
+ if [ $? -ne 0 ]; then
+ echo ==== Error backing up DB $1 to $2 ====
+ exit 1
+ fi
+ set -e
+}
+
+restore_db()
+{
+ set +e
+ bash "$script_copy_dir"/restore_db.sh "$1" "$2"
+ if [ $? -ne 0 ]; then
+ echo ==== Error restoring from $1 to $2 ====
+ exit 1
+ fi
+ set -e
+}
+
+member_of_array()
+{
+ local e match="$1"
+ shift
+ for e; do [[ "$e" == "$match" ]] && return 0; done
+ return 1
+}
+
+force_no_fbcode()
+{
+ # Not all branches recognize ROCKSDB_NO_FBCODE and we should not need
+ # to patch old branches for changes to available FB compilers.
+ sed -i -e 's|-d /mnt/gvfs/third-party|"$ROCKSDB_FORCE_FBCODE"|' build_tools/build_detect_platform
+}
+
+# General structure from here:
+# * Check out, build, and do stuff with the "current" branch.
+# * For each older branch under consideration,
+# * Check out, build, and do stuff with it, potentially using data
+# generated from "current" branch.
+# * (Again) check out, build, and do (other) stuff with the "current"
+# branch, potentially using data from older branches.
+#
+# This way, we only do at most n+1 checkout+build steps, without the
+# need to stash away executables.
+
+# Decorate name
+current_checkout_name="$current_checkout_name ($current_checkout_hash)"
+
+echo "== Building $current_checkout_name debug"
+git checkout -B $tmp_branch $current_checkout_hash
+force_no_fbcode
+make clean
+DISABLE_WARNING_AS_ERROR=1 make ldb -j32
+
+echo "== Using $current_checkout_name, generate DB with extern SST and ingest"
+current_ext_test_dir=$ext_test_dir"/current"
+write_external_sst $input_data_path ${current_ext_test_dir}_pointless $current_ext_test_dir
+ingest_external_sst ${current_ext_test_dir}_ingest $current_ext_test_dir
+
+echo "== Generating DB from $current_checkout_name ..."
+current_db_test_dir=$db_test_dir"/current"
+generate_db $input_data_path $current_db_test_dir
+
+echo "== Creating backup of DB from $current_checkout_name ..."
+current_bak_test_dir=$bak_test_dir"/current"
+backup_db $current_db_test_dir $current_bak_test_dir
+
+for checkout_ref in "${checkout_refs[@]}"
+do
+ echo "== Building $checkout_ref debug"
+ git reset --hard $tmp_origin/$checkout_ref
+ force_no_fbcode
+ make clean
+ DISABLE_WARNING_AS_ERROR=1 make ldb -j32
+
+ # We currently assume DB backward compatibility for every branch listed
+ echo "== Use $checkout_ref to generate a DB ..."
+ generate_db $input_data_path $db_test_dir/$checkout_ref
+
+ if member_of_array "$checkout_ref" "${ext_backward_only_refs[@]}" ||
+ member_of_array "$checkout_ref" "${ext_forward_refs[@]}"
+ then
+ echo "== Use $checkout_ref to generate DB with extern SST file"
+ write_external_sst $input_data_path $ext_test_dir/${checkout_ref}_pointless $ext_test_dir/$checkout_ref
+ fi
+
+ if member_of_array "$checkout_ref" "${ext_forward_refs[@]}"
+ then
+ echo "== Use $checkout_ref to ingest extern SST file and compare vs. $current_checkout_name"
+ ingest_external_sst $ext_test_dir/${checkout_ref}_ingest $ext_test_dir/$checkout_ref
+ compare_db $ext_test_dir/${checkout_ref}_ingest ${current_ext_test_dir}_ingest db_dump.txt 1 1
+
+ rm -rf ${ext_test_dir:?}/${checkout_ref}_ingest
+ echo "== Use $checkout_ref to ingest extern SST file from $current_checkout_name"
+ ingest_external_sst $ext_test_dir/${checkout_ref}_ingest $current_ext_test_dir
+ compare_db $ext_test_dir/${checkout_ref}_ingest ${current_ext_test_dir}_ingest db_dump.txt 1 1
+ fi
+
+ if member_of_array "$checkout_ref" "${db_forward_no_options_refs[@]}" ||
+ member_of_array "$checkout_ref" "${db_forward_with_options_refs[@]}"
+ then
+ echo "== Use $checkout_ref to open DB generated using $current_checkout_name..."
+ compare_db $db_test_dir/$checkout_ref $current_db_test_dir forward_${checkout_ref}_dump.txt 0
+ fi
+
+ if member_of_array "$checkout_ref" "${db_forward_with_options_refs[@]}"
+ then
+ echo "== Use $checkout_ref to open DB generated using $current_checkout_name with its options..."
+ compare_db $db_test_dir/$checkout_ref $current_db_test_dir forward_${checkout_ref}_dump.txt 1 1
+ fi
+
+ if member_of_array "$checkout_ref" "${bak_backward_only_refs[@]}" ||
+ member_of_array "$checkout_ref" "${bak_forward_refs[@]}"
+ then
+ echo "== Use $checkout_ref to backup DB"
+ backup_db $db_test_dir/$checkout_ref $bak_test_dir/$checkout_ref
+ fi
+
+ if member_of_array "$checkout_ref" "${bak_forward_refs[@]}"
+ then
+ echo "== Use $checkout_ref to restore DB from $current_checkout_name"
+ rm -rf ${db_test_dir:?}/$checkout_ref
+ restore_db $current_bak_test_dir $db_test_dir/$checkout_ref
+ compare_db $db_test_dir/$checkout_ref $current_db_test_dir forward_${checkout_ref}_dump.txt 0
+ fi
+done
+
+echo "== Building $current_checkout_name debug (again, final)"
+git reset --hard $current_checkout_hash
+force_no_fbcode
+make clean
+DISABLE_WARNING_AS_ERROR=1 make ldb -j32
+
+for checkout_ref in "${checkout_refs[@]}"
+do
+ # We currently assume DB backward compatibility for every branch listed
+ echo "== Use $current_checkout_name to open DB generated using $checkout_ref..."
+ compare_db $db_test_dir/$checkout_ref $current_db_test_dir db_dump.txt 1 0
+
+ if member_of_array "$checkout_ref" "${ext_backward_only_refs[@]}" ||
+ member_of_array "$checkout_ref" "${ext_forward_refs[@]}"
+ then
+ rm -rf ${ext_test_dir:?}/${checkout_ref}_ingest
+ echo "== Use $current_checkout_name to ingest extern SST file from $checkout_ref"
+ ingest_external_sst $ext_test_dir/${checkout_ref}_ingest $current_ext_test_dir
+ compare_db $ext_test_dir/${checkout_ref}_ingest ${current_ext_test_dir}_ingest db_dump.txt 1 1
+ fi
+
+ if member_of_array "$checkout_ref" "${bak_backward_only_refs[@]}" ||
+ member_of_array "$checkout_ref" "${bak_forward_refs[@]}"
+ then
+ echo "== Use $current_checkout_name to restore DB from $checkout_ref"
+ rm -rf ${db_test_dir:?}/$checkout_ref
+ restore_db $bak_test_dir/$checkout_ref $db_test_dir/$checkout_ref
+ compare_db $db_test_dir/$checkout_ref $current_db_test_dir db_dump.txt 1 0
+ fi
+done
+
+echo ==== Compatibility Test PASSED ====
diff --git a/src/rocksdb/tools/db_bench.cc b/src/rocksdb/tools/db_bench.cc
new file mode 100644
index 000000000..f13de83fe
--- /dev/null
+++ b/src/rocksdb/tools/db_bench.cc
@@ -0,0 +1,21 @@
+// Copyright (c) 2013-present, Facebook, Inc. All rights reserved.
+// This source code is licensed under both the GPLv2 (found in the
+// COPYING file in the root directory) and Apache 2.0 License
+// (found in the LICENSE.Apache file in the root directory).
+//
+// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file. See the AUTHORS file for names of contributors.
+
+#ifndef GFLAGS
+#include <cstdio>
+int main() {
+ fprintf(stderr, "Please install gflags to run rocksdb tools\n");
+ return 1;
+}
+#else
+#include "rocksdb/db_bench_tool.h"
+int main(int argc, char** argv) {
+ return ROCKSDB_NAMESPACE::db_bench_tool(argc, argv);
+}
+#endif // GFLAGS
diff --git a/src/rocksdb/tools/db_bench_tool.cc b/src/rocksdb/tools/db_bench_tool.cc
new file mode 100644
index 000000000..7182528b3
--- /dev/null
+++ b/src/rocksdb/tools/db_bench_tool.cc
@@ -0,0 +1,8707 @@
+// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+// This source code is licensed under both the GPLv2 (found in the
+// COPYING file in the root directory) and Apache 2.0 License
+// (found in the LICENSE.Apache file in the root directory).
+//
+// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file. See the AUTHORS file for names of contributors.
+
+#ifdef GFLAGS
+#ifdef NUMA
+#include <numa.h>
+#endif
+#ifndef OS_WIN
+#include <unistd.h>
+#endif
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/types.h>
+#ifdef __APPLE__
+#include <mach/host_info.h>
+#include <mach/mach_host.h>
+#include <sys/sysctl.h>
+#endif
+#ifdef __FreeBSD__
+#include <sys/sysctl.h>
+#endif
+#include <atomic>
+#include <cinttypes>
+#include <condition_variable>
+#include <cstddef>
+#include <iostream>
+#include <memory>
+#include <mutex>
+#include <queue>
+#include <thread>
+#include <unordered_map>
+
+#include "db/db_impl/db_impl.h"
+#include "db/malloc_stats.h"
+#include "db/version_set.h"
+#include "monitoring/histogram.h"
+#include "monitoring/statistics.h"
+#include "options/cf_options.h"
+#include "port/port.h"
+#include "port/stack_trace.h"
+#include "rocksdb/cache.h"
+#include "rocksdb/convenience.h"
+#include "rocksdb/db.h"
+#include "rocksdb/env.h"
+#include "rocksdb/filter_policy.h"
+#include "rocksdb/memtablerep.h"
+#include "rocksdb/options.h"
+#include "rocksdb/perf_context.h"
+#include "rocksdb/persistent_cache.h"
+#include "rocksdb/rate_limiter.h"
+#include "rocksdb/secondary_cache.h"
+#include "rocksdb/slice.h"
+#include "rocksdb/slice_transform.h"
+#include "rocksdb/stats_history.h"
+#include "rocksdb/table.h"
+#include "rocksdb/utilities/backup_engine.h"
+#include "rocksdb/utilities/object_registry.h"
+#include "rocksdb/utilities/optimistic_transaction_db.h"
+#include "rocksdb/utilities/options_type.h"
+#include "rocksdb/utilities/options_util.h"
+#ifndef ROCKSDB_LITE
+#include "rocksdb/utilities/replayer.h"
+#endif // ROCKSDB_LITE
+#include "rocksdb/utilities/sim_cache.h"
+#include "rocksdb/utilities/transaction.h"
+#include "rocksdb/utilities/transaction_db.h"
+#include "rocksdb/write_batch.h"
+#include "test_util/testutil.h"
+#include "test_util/transaction_test_util.h"
+#include "tools/simulated_hybrid_file_system.h"
+#include "util/cast_util.h"
+#include "util/compression.h"
+#include "util/crc32c.h"
+#include "util/file_checksum_helper.h"
+#include "util/gflags_compat.h"
+#include "util/mutexlock.h"
+#include "util/random.h"
+#include "util/stderr_logger.h"
+#include "util/string_util.h"
+#include "util/xxhash.h"
+#include "utilities/blob_db/blob_db.h"
+#include "utilities/counted_fs.h"
+#include "utilities/merge_operators.h"
+#include "utilities/merge_operators/bytesxor.h"
+#include "utilities/merge_operators/sortlist.h"
+#include "utilities/persistent_cache/block_cache_tier.h"
+
+#ifdef MEMKIND
+#include "memory/memkind_kmem_allocator.h"
+#endif
+
+#ifdef OS_WIN
+#include <io.h> // open/close
+#endif
+
+using GFLAGS_NAMESPACE::ParseCommandLineFlags;
+using GFLAGS_NAMESPACE::RegisterFlagValidator;
+using GFLAGS_NAMESPACE::SetUsageMessage;
+using GFLAGS_NAMESPACE::SetVersionString;
+
+#ifdef ROCKSDB_LITE
+#define IF_ROCKSDB_LITE(Then, Else) Then
+#else
+#define IF_ROCKSDB_LITE(Then, Else) Else
+#endif
+
+DEFINE_string(
+ benchmarks,
+ "fillseq,"
+ "fillseqdeterministic,"
+ "fillsync,"
+ "fillrandom,"
+ "filluniquerandomdeterministic,"
+ "overwrite,"
+ "readrandom,"
+ "newiterator,"
+ "newiteratorwhilewriting,"
+ "seekrandom,"
+ "seekrandomwhilewriting,"
+ "seekrandomwhilemerging,"
+ "readseq,"
+ "readreverse,"
+ "compact,"
+ "compactall,"
+ "flush,"
+IF_ROCKSDB_LITE("",
+ "compact0,"
+ "compact1,"
+ "waitforcompaction,"
+)
+ "multireadrandom,"
+ "mixgraph,"
+ "readseq,"
+ "readtorowcache,"
+ "readtocache,"
+ "readreverse,"
+ "readwhilewriting,"
+ "readwhilemerging,"
+ "readwhilescanning,"
+ "readrandomwriterandom,"
+ "updaterandom,"
+ "xorupdaterandom,"
+ "approximatesizerandom,"
+ "randomwithverify,"
+ "fill100K,"
+ "crc32c,"
+ "xxhash,"
+ "xxhash64,"
+ "xxh3,"
+ "compress,"
+ "uncompress,"
+ "acquireload,"
+ "fillseekseq,"
+ "randomtransaction,"
+ "randomreplacekeys,"
+ "timeseries,"
+ "getmergeoperands,",
+ "readrandomoperands,"
+ "backup,"
+ "restore"
+
+ "Comma-separated list of operations to run in the specified"
+ " order. Available benchmarks:\n"
+ "\tfillseq -- write N values in sequential key"
+ " order in async mode\n"
+ "\tfillseqdeterministic -- write N values in the specified"
+ " key order and keep the shape of the LSM tree\n"
+ "\tfillrandom -- write N values in random key order in async"
+ " mode\n"
+ "\tfilluniquerandomdeterministic -- write N values in a random"
+ " key order and keep the shape of the LSM tree\n"
+ "\toverwrite -- overwrite N values in random key order in "
+ "async mode\n"
+ "\tfillsync -- write N/1000 values in random key order in "
+ "sync mode\n"
+ "\tfill100K -- write N/1000 100K values in random order in"
+ " async mode\n"
+ "\tdeleteseq -- delete N keys in sequential order\n"
+ "\tdeleterandom -- delete N keys in random order\n"
+ "\treadseq -- read N times sequentially\n"
+ "\treadtocache -- 1 thread reading database sequentially\n"
+ "\treadreverse -- read N times in reverse order\n"
+ "\treadrandom -- read N times in random order\n"
+ "\treadmissing -- read N missing keys in random order\n"
+ "\treadwhilewriting -- 1 writer, N threads doing random "
+ "reads\n"
+ "\treadwhilemerging -- 1 merger, N threads doing random "
+ "reads\n"
+ "\treadwhilescanning -- 1 thread doing full table scan, "
+ "N threads doing random reads\n"
+ "\treadrandomwriterandom -- N threads doing random-read, "
+ "random-write\n"
+ "\tupdaterandom -- N threads doing read-modify-write for random "
+ "keys\n"
+ "\txorupdaterandom -- N threads doing read-XOR-write for "
+ "random keys\n"
+ "\tappendrandom -- N threads doing read-modify-write with "
+ "growing values\n"
+ "\tmergerandom -- same as updaterandom/appendrandom using merge"
+ " operator. "
+ "Must be used with merge_operator\n"
+ "\treadrandommergerandom -- perform N random read-or-merge "
+ "operations. Must be used with merge_operator\n"
+ "\tnewiterator -- repeated iterator creation\n"
+ "\tseekrandom -- N random seeks, call Next seek_nexts times "
+ "per seek\n"
+ "\tseekrandomwhilewriting -- seekrandom and 1 thread doing "
+ "overwrite\n"
+ "\tseekrandomwhilemerging -- seekrandom and 1 thread doing "
+ "merge\n"
+ "\tcrc32c -- repeated crc32c of <block size> data\n"
+ "\txxhash -- repeated xxHash of <block size> data\n"
+ "\txxhash64 -- repeated xxHash64 of <block size> data\n"
+ "\txxh3 -- repeated XXH3 of <block size> data\n"
+ "\tacquireload -- load N*1000 times\n"
+ "\tfillseekseq -- write N values in sequential key, then read "
+ "them by seeking to each key\n"
+ "\trandomtransaction -- execute N random transactions and "
+ "verify correctness\n"
+ "\trandomreplacekeys -- randomly replaces N keys by deleting "
+ "the old version and putting the new version\n\n"
+ "\ttimeseries -- 1 writer generates time series data "
+ "and multiple readers doing random reads on id\n\n"
+ "Meta operations:\n"
+ "\tcompact -- Compact the entire DB; If multiple, randomly choose one\n"
+ "\tcompactall -- Compact the entire DB\n"
+IF_ROCKSDB_LITE("",
+ "\tcompact0 -- compact L0 into L1\n"
+ "\tcompact1 -- compact L1 into L2\n"
+ "\twaitforcompaction - pause until compaction is (probably) done\n"
+)
+ "\tflush - flush the memtable\n"
+ "\tstats -- Print DB stats\n"
+ "\tresetstats -- Reset DB stats\n"
+ "\tlevelstats -- Print the number of files and bytes per level\n"
+ "\tmemstats -- Print memtable stats\n"
+ "\tsstables -- Print sstable info\n"
+ "\theapprofile -- Dump a heap profile (if supported by this port)\n"
+IF_ROCKSDB_LITE("",
+ "\treplay -- replay the trace file specified with trace_file\n"
+)
+ "\tgetmergeoperands -- Insert lots of merge records which are a list of "
+ "sorted ints for a key and then compare performance of lookup for another "
+ "key by doing a Get followed by binary searching in the large sorted list "
+ "vs doing a GetMergeOperands and binary searching in the operands which "
+ "are sorted sub-lists. The MergeOperator used is sortlist.h\n"
+ "\treadrandomoperands -- read random keys using `GetMergeOperands()`. An "
+ "operation includes a rare but possible retry in case it got "
+ "`Status::Incomplete()`. This happens upon encountering more keys than "
+ "have ever been seen by the thread (or eight initially)\n"
+ "\tbackup -- Create a backup of the current DB and verify that a new backup is corrected. "
+ "Rate limit can be specified through --backup_rate_limit\n"
+ "\trestore -- Restore the DB from the latest backup available, rate limit can be specified through --restore_rate_limit\n");
+
+DEFINE_int64(num, 1000000, "Number of key/values to place in database");
+
+DEFINE_int64(numdistinct, 1000,
+ "Number of distinct keys to use. Used in RandomWithVerify to "
+ "read/write on fewer keys so that gets are more likely to find the"
+ " key and puts are more likely to update the same key");
+
+DEFINE_int64(merge_keys, -1,
+ "Number of distinct keys to use for MergeRandom and "
+ "ReadRandomMergeRandom. "
+ "If negative, there will be FLAGS_num keys.");
+DEFINE_int32(num_column_families, 1, "Number of Column Families to use.");
+
+DEFINE_int32(
+ num_hot_column_families, 0,
+ "Number of Hot Column Families. If more than 0, only write to this "
+ "number of column families. After finishing all the writes to them, "
+ "create new set of column families and insert to them. Only used "
+ "when num_column_families > 1.");
+
+DEFINE_string(column_family_distribution, "",
+ "Comma-separated list of percentages, where the ith element "
+ "indicates the probability of an op using the ith column family. "
+ "The number of elements must be `num_hot_column_families` if "
+ "specified; otherwise, it must be `num_column_families`. The "
+ "sum of elements must be 100. E.g., if `num_column_families=4`, "
+ "and `num_hot_column_families=0`, a valid list could be "
+ "\"10,20,30,40\".");
+
+DEFINE_int64(reads, -1,
+ "Number of read operations to do. "
+ "If negative, do FLAGS_num reads.");
+
+DEFINE_int64(deletes, -1,
+ "Number of delete operations to do. "
+ "If negative, do FLAGS_num deletions.");
+
+DEFINE_int32(bloom_locality, 0, "Control bloom filter probes locality");
+
+DEFINE_int64(seed, 0,
+ "Seed base for random number generators. "
+ "When 0 it is derived from the current time.");
+static int64_t seed_base;
+
+DEFINE_int32(threads, 1, "Number of concurrent threads to run.");
+
+DEFINE_int32(duration, 0,
+ "Time in seconds for the random-ops tests to run."
+ " When 0 then num & reads determine the test duration");
+
+DEFINE_string(value_size_distribution_type, "fixed",
+ "Value size distribution type: fixed, uniform, normal");
+
+DEFINE_int32(value_size, 100, "Size of each value in fixed distribution");
+static unsigned int value_size = 100;
+
+DEFINE_int32(value_size_min, 100, "Min size of random value");
+
+DEFINE_int32(value_size_max, 102400, "Max size of random value");
+
+DEFINE_int32(seek_nexts, 0,
+ "How many times to call Next() after Seek() in "
+ "fillseekseq, seekrandom, seekrandomwhilewriting and "
+ "seekrandomwhilemerging");
+
+DEFINE_bool(reverse_iterator, false,
+ "When true use Prev rather than Next for iterators that do "
+ "Seek and then Next");
+
+DEFINE_bool(auto_prefix_mode, false, "Set auto_prefix_mode for seek benchmark");
+
+DEFINE_int64(max_scan_distance, 0,
+ "Used to define iterate_upper_bound (or iterate_lower_bound "
+ "if FLAGS_reverse_iterator is set to true) when value is nonzero");
+
+DEFINE_bool(use_uint64_comparator, false, "use Uint64 user comparator");
+
+DEFINE_int64(batch_size, 1, "Batch size");
+
+static bool ValidateKeySize(const char* /*flagname*/, int32_t /*value*/) {
+ return true;
+}
+
+static bool ValidateUint32Range(const char* flagname, uint64_t value) {
+ if (value > std::numeric_limits<uint32_t>::max()) {
+ fprintf(stderr, "Invalid value for --%s: %lu, overflow\n", flagname,
+ (unsigned long)value);
+ return false;
+ }
+ return true;
+}
+
+DEFINE_int32(key_size, 16, "size of each key");
+
+DEFINE_int32(user_timestamp_size, 0,
+ "number of bytes in a user-defined timestamp");
+
+DEFINE_int32(num_multi_db, 0,
+ "Number of DBs used in the benchmark. 0 means single DB.");
+
+DEFINE_double(compression_ratio, 0.5,
+ "Arrange to generate values that shrink to this fraction of "
+ "their original size after compression");
+
+DEFINE_double(
+ overwrite_probability, 0.0,
+ "Used in 'filluniquerandom' benchmark: for each write operation, "
+ "we give a probability to perform an overwrite instead. The key used for "
+ "the overwrite is randomly chosen from the last 'overwrite_window_size' "
+ "keys previously inserted into the DB. "
+ "Valid overwrite_probability values: [0.0, 1.0].");
+
+DEFINE_uint32(overwrite_window_size, 1,
+ "Used in 'filluniquerandom' benchmark. For each write operation,"
+ " when the overwrite_probability flag is set by the user, the "
+ "key used to perform an overwrite is randomly chosen from the "
+ "last 'overwrite_window_size' keys previously inserted into DB. "
+ "Warning: large values can affect throughput. "
+ "Valid overwrite_window_size values: [1, kMaxUint32].");
+
+DEFINE_uint64(
+ disposable_entries_delete_delay, 0,
+ "Minimum delay in microseconds for the series of Deletes "
+ "to be issued. When 0 the insertion of the last disposable entry is "
+ "immediately followed by the issuance of the Deletes. "
+ "(only compatible with fillanddeleteuniquerandom benchmark).");
+
+DEFINE_uint64(disposable_entries_batch_size, 0,
+ "Number of consecutively inserted disposable KV entries "
+ "that will be deleted after 'delete_delay' microseconds. "
+ "A series of Deletes is always issued once all the "
+ "disposable KV entries it targets have been inserted "
+ "into the DB. When 0 no deletes are issued and a "
+ "regular 'filluniquerandom' benchmark occurs. "
+ "(only compatible with fillanddeleteuniquerandom benchmark)");
+
+DEFINE_int32(disposable_entries_value_size, 64,
+ "Size of the values (in bytes) of the entries targeted by "
+ "selective deletes. "
+ "(only compatible with fillanddeleteuniquerandom benchmark)");
+
+DEFINE_uint64(
+ persistent_entries_batch_size, 0,
+ "Number of KV entries being inserted right before the deletes "
+ "targeting the disposable KV entries are issued. These "
+ "persistent keys are not targeted by the deletes, and will always "
+ "remain valid in the DB. (only compatible with "
+ "--benchmarks='fillanddeleteuniquerandom' "
+ "and used when--disposable_entries_batch_size is > 0).");
+
+DEFINE_int32(persistent_entries_value_size, 64,
+ "Size of the values (in bytes) of the entries not targeted by "
+ "deletes. (only compatible with "
+ "--benchmarks='fillanddeleteuniquerandom' "
+ "and used when--disposable_entries_batch_size is > 0).");
+
+DEFINE_double(read_random_exp_range, 0.0,
+ "Read random's key will be generated using distribution of "
+ "num * exp(-r) where r is uniform number from 0 to this value. "
+ "The larger the number is, the more skewed the reads are. "
+ "Only used in readrandom and multireadrandom benchmarks.");
+
+DEFINE_bool(histogram, false, "Print histogram of operation timings");
+
+DEFINE_bool(confidence_interval_only, false,
+ "Print 95% confidence interval upper and lower bounds only for "
+ "aggregate stats.");
+
+DEFINE_bool(enable_numa, false,
+ "Make operations aware of NUMA architecture and bind memory "
+ "and cpus corresponding to nodes together. In NUMA, memory "
+ "in same node as CPUs are closer when compared to memory in "
+ "other nodes. Reads can be faster when the process is bound to "
+ "CPU and memory of same node. Use \"$numactl --hardware\" command "
+ "to see NUMA memory architecture.");
+
+DEFINE_int64(db_write_buffer_size,
+ ROCKSDB_NAMESPACE::Options().db_write_buffer_size,
+ "Number of bytes to buffer in all memtables before compacting");
+
+DEFINE_bool(cost_write_buffer_to_cache, false,
+ "The usage of memtable is costed to the block cache");
+
+DEFINE_int64(arena_block_size, ROCKSDB_NAMESPACE::Options().arena_block_size,
+ "The size, in bytes, of one block in arena memory allocation.");
+
+DEFINE_int64(write_buffer_size, ROCKSDB_NAMESPACE::Options().write_buffer_size,
+ "Number of bytes to buffer in memtable before compacting");
+
+DEFINE_int32(max_write_buffer_number,
+ ROCKSDB_NAMESPACE::Options().max_write_buffer_number,
+ "The number of in-memory memtables. Each memtable is of size"
+ " write_buffer_size bytes.");
+
+DEFINE_int32(min_write_buffer_number_to_merge,
+ ROCKSDB_NAMESPACE::Options().min_write_buffer_number_to_merge,
+ "The minimum number of write buffers that will be merged together"
+ "before writing to storage. This is cheap because it is an"
+ "in-memory merge. If this feature is not enabled, then all these"
+ "write buffers are flushed to L0 as separate files and this "
+ "increases read amplification because a get request has to check"
+ " in all of these files. Also, an in-memory merge may result in"
+ " writing less data to storage if there are duplicate records "
+ " in each of these individual write buffers.");
+
+DEFINE_int32(max_write_buffer_number_to_maintain,
+ ROCKSDB_NAMESPACE::Options().max_write_buffer_number_to_maintain,
+ "The total maximum number of write buffers to maintain in memory "
+ "including copies of buffers that have already been flushed. "
+ "Unlike max_write_buffer_number, this parameter does not affect "
+ "flushing. This controls the minimum amount of write history "
+ "that will be available in memory for conflict checking when "
+ "Transactions are used. If this value is too low, some "
+ "transactions may fail at commit time due to not being able to "
+ "determine whether there were any write conflicts. Setting this "
+ "value to 0 will cause write buffers to be freed immediately "
+ "after they are flushed. If this value is set to -1, "
+ "'max_write_buffer_number' will be used.");
+
+DEFINE_int64(max_write_buffer_size_to_maintain,
+ ROCKSDB_NAMESPACE::Options().max_write_buffer_size_to_maintain,
+ "The total maximum size of write buffers to maintain in memory "
+ "including copies of buffers that have already been flushed. "
+ "Unlike max_write_buffer_number, this parameter does not affect "
+ "flushing. This controls the minimum amount of write history "
+ "that will be available in memory for conflict checking when "
+ "Transactions are used. If this value is too low, some "
+ "transactions may fail at commit time due to not being able to "
+ "determine whether there were any write conflicts. Setting this "
+ "value to 0 will cause write buffers to be freed immediately "
+ "after they are flushed. If this value is set to -1, "
+ "'max_write_buffer_number' will be used.");
+
+DEFINE_int32(max_background_jobs,
+ ROCKSDB_NAMESPACE::Options().max_background_jobs,
+ "The maximum number of concurrent background jobs that can occur "
+ "in parallel.");
+
+DEFINE_int32(num_bottom_pri_threads, 0,
+ "The number of threads in the bottom-priority thread pool (used "
+ "by universal compaction only).");
+
+DEFINE_int32(num_high_pri_threads, 0,
+ "The maximum number of concurrent background compactions"
+ " that can occur in parallel.");
+
+DEFINE_int32(num_low_pri_threads, 0,
+ "The maximum number of concurrent background compactions"
+ " that can occur in parallel.");
+
+DEFINE_int32(max_background_compactions,
+ ROCKSDB_NAMESPACE::Options().max_background_compactions,
+ "The maximum number of concurrent background compactions"
+ " that can occur in parallel.");
+
+DEFINE_uint64(subcompactions, 1,
+ "Maximum number of subcompactions to divide L0-L1 compactions "
+ "into.");
+static const bool FLAGS_subcompactions_dummy __attribute__((__unused__)) =
+ RegisterFlagValidator(&FLAGS_subcompactions, &ValidateUint32Range);
+
+DEFINE_int32(max_background_flushes,
+ ROCKSDB_NAMESPACE::Options().max_background_flushes,
+ "The maximum number of concurrent background flushes"
+ " that can occur in parallel.");
+
+static ROCKSDB_NAMESPACE::CompactionStyle FLAGS_compaction_style_e;
+DEFINE_int32(compaction_style,
+ (int32_t)ROCKSDB_NAMESPACE::Options().compaction_style,
+ "style of compaction: level-based, universal and fifo");
+
+static ROCKSDB_NAMESPACE::CompactionPri FLAGS_compaction_pri_e;
+DEFINE_int32(compaction_pri,
+ (int32_t)ROCKSDB_NAMESPACE::Options().compaction_pri,
+ "priority of files to compaction: by size or by data age");
+
+DEFINE_int32(universal_size_ratio, 0,
+ "Percentage flexibility while comparing file size "
+ "(for universal compaction only).");
+
+DEFINE_int32(universal_min_merge_width, 0,
+ "The minimum number of files in a single compaction run "
+ "(for universal compaction only).");
+
+DEFINE_int32(universal_max_merge_width, 0,
+ "The max number of files to compact in universal style "
+ "compaction");
+
+DEFINE_int32(universal_max_size_amplification_percent, 0,
+ "The max size amplification for universal style compaction");
+
+DEFINE_int32(universal_compression_size_percent, -1,
+ "The percentage of the database to compress for universal "
+ "compaction. -1 means compress everything.");
+
+DEFINE_bool(universal_allow_trivial_move, false,
+ "Allow trivial move in universal compaction.");
+
+DEFINE_bool(universal_incremental, false,
+ "Enable incremental compactions in universal compaction.");
+
+DEFINE_int64(cache_size, 8 << 20, // 8MB
+ "Number of bytes to use as a cache of uncompressed data");
+
+DEFINE_int32(cache_numshardbits, -1,
+ "Number of shards for the block cache"
+ " is 2 ** cache_numshardbits. Negative means use default settings."
+ " This is applied only if FLAGS_cache_size is non-negative.");
+
+DEFINE_double(cache_high_pri_pool_ratio, 0.0,
+ "Ratio of block cache reserve for high pri blocks. "
+ "If > 0.0, we also enable "
+ "cache_index_and_filter_blocks_with_high_priority.");
+
+DEFINE_double(cache_low_pri_pool_ratio, 0.0,
+ "Ratio of block cache reserve for low pri blocks.");
+
+DEFINE_string(cache_type, "lru_cache", "Type of block cache.");
+
+DEFINE_bool(use_compressed_secondary_cache, false,
+ "Use the CompressedSecondaryCache as the secondary cache.");
+
+DEFINE_int64(compressed_secondary_cache_size, 8 << 20, // 8MB
+ "Number of bytes to use as a cache of data");
+
+DEFINE_int32(compressed_secondary_cache_numshardbits, 6,
+ "Number of shards for the block cache"
+ " is 2 ** compressed_secondary_cache_numshardbits."
+ " Negative means use default settings."
+ " This is applied only if FLAGS_cache_size is non-negative.");
+
+DEFINE_double(compressed_secondary_cache_high_pri_pool_ratio, 0.0,
+ "Ratio of block cache reserve for high pri blocks. "
+ "If > 0.0, we also enable "
+ "cache_index_and_filter_blocks_with_high_priority.");
+
+DEFINE_double(compressed_secondary_cache_low_pri_pool_ratio, 0.0,
+ "Ratio of block cache reserve for low pri blocks.");
+
+DEFINE_string(compressed_secondary_cache_compression_type, "lz4",
+ "The compression algorithm to use for large "
+ "values stored in CompressedSecondaryCache.");
+static enum ROCKSDB_NAMESPACE::CompressionType
+ FLAGS_compressed_secondary_cache_compression_type_e =
+ ROCKSDB_NAMESPACE::kLZ4Compression;
+
+DEFINE_uint32(
+ compressed_secondary_cache_compress_format_version, 2,
+ "compress_format_version can have two values: "
+ "compress_format_version == 1 -- decompressed size is not included"
+ " in the block header."
+ "compress_format_version == 2 -- decompressed size is included"
+ " in the block header in varint32 format.");
+
+DEFINE_int64(simcache_size, -1,
+ "Number of bytes to use as a simcache of "
+ "uncompressed data. Nagative value disables simcache.");
+
+DEFINE_bool(cache_index_and_filter_blocks, false,
+ "Cache index/filter blocks in block cache.");
+
+DEFINE_bool(use_cache_jemalloc_no_dump_allocator, false,
+ "Use JemallocNodumpAllocator for block/blob cache.");
+
+DEFINE_bool(use_cache_memkind_kmem_allocator, false,
+ "Use memkind kmem allocator for block/blob cache.");
+
+DEFINE_bool(partition_index_and_filters, false,
+ "Partition index and filter blocks.");
+
+DEFINE_bool(partition_index, false, "Partition index blocks");
+
+DEFINE_bool(index_with_first_key, false, "Include first key in the index");
+
+DEFINE_bool(
+ optimize_filters_for_memory,
+ ROCKSDB_NAMESPACE::BlockBasedTableOptions().optimize_filters_for_memory,
+ "Minimize memory footprint of filters");
+
+DEFINE_int64(
+ index_shortening_mode, 2,
+ "mode to shorten index: 0 for no shortening; 1 for only shortening "
+ "separaters; 2 for shortening shortening and successor");
+
+DEFINE_int64(metadata_block_size,
+ ROCKSDB_NAMESPACE::BlockBasedTableOptions().metadata_block_size,
+ "Max partition size when partitioning index/filters");
+
+// The default reduces the overhead of reading time with flash. With HDD, which
+// offers much less throughput, however, this number better to be set to 1.
+DEFINE_int32(ops_between_duration_checks, 1000,
+ "Check duration limit every x ops");
+
+DEFINE_bool(pin_l0_filter_and_index_blocks_in_cache, false,
+ "Pin index/filter blocks of L0 files in block cache.");
+
+DEFINE_bool(
+ pin_top_level_index_and_filter, false,
+ "Pin top-level index of partitioned index/filter blocks in block cache.");
+
+DEFINE_int32(block_size,
+ static_cast<int32_t>(
+ ROCKSDB_NAMESPACE::BlockBasedTableOptions().block_size),
+ "Number of bytes in a block.");
+
+DEFINE_int32(format_version,
+ static_cast<int32_t>(
+ ROCKSDB_NAMESPACE::BlockBasedTableOptions().format_version),
+ "Format version of SST files.");
+
+DEFINE_int32(block_restart_interval,
+ ROCKSDB_NAMESPACE::BlockBasedTableOptions().block_restart_interval,
+ "Number of keys between restart points "
+ "for delta encoding of keys in data block.");
+
+DEFINE_int32(
+ index_block_restart_interval,
+ ROCKSDB_NAMESPACE::BlockBasedTableOptions().index_block_restart_interval,
+ "Number of keys between restart points "
+ "for delta encoding of keys in index block.");
+
+DEFINE_int32(read_amp_bytes_per_bit,
+ ROCKSDB_NAMESPACE::BlockBasedTableOptions().read_amp_bytes_per_bit,
+ "Number of bytes per bit to be used in block read-amp bitmap");
+
+DEFINE_bool(
+ enable_index_compression,
+ ROCKSDB_NAMESPACE::BlockBasedTableOptions().enable_index_compression,
+ "Compress the index block");
+
+DEFINE_bool(block_align,
+ ROCKSDB_NAMESPACE::BlockBasedTableOptions().block_align,
+ "Align data blocks on page size");
+
+DEFINE_int64(prepopulate_block_cache, 0,
+ "Pre-populate hot/warm blocks in block cache. 0 to disable and 1 "
+ "to insert during flush");
+
+DEFINE_bool(use_data_block_hash_index, false,
+ "if use kDataBlockBinaryAndHash "
+ "instead of kDataBlockBinarySearch. "
+ "This is valid if only we use BlockTable");
+
+DEFINE_double(data_block_hash_table_util_ratio, 0.75,
+ "util ratio for data block hash index table. "
+ "This is only valid if use_data_block_hash_index is "
+ "set to true");
+
+DEFINE_int64(compressed_cache_size, -1,
+ "Number of bytes to use as a cache of compressed data.");
+
+DEFINE_int64(row_cache_size, 0,
+ "Number of bytes to use as a cache of individual rows"
+ " (0 = disabled).");
+
+DEFINE_int32(open_files, ROCKSDB_NAMESPACE::Options().max_open_files,
+ "Maximum number of files to keep open at the same time"
+ " (use default if == 0)");
+
+DEFINE_int32(file_opening_threads,
+ ROCKSDB_NAMESPACE::Options().max_file_opening_threads,
+ "If open_files is set to -1, this option set the number of "
+ "threads that will be used to open files during DB::Open()");
+
+DEFINE_int32(compaction_readahead_size, 0, "Compaction readahead size");
+
+DEFINE_int32(log_readahead_size, 0, "WAL and manifest readahead size");
+
+DEFINE_int32(random_access_max_buffer_size, 1024 * 1024,
+ "Maximum windows randomaccess buffer size");
+
+DEFINE_int32(writable_file_max_buffer_size, 1024 * 1024,
+ "Maximum write buffer for Writable File");
+
+DEFINE_int32(bloom_bits, -1,
+ "Bloom filter bits per key. Negative means use default."
+ "Zero disables.");
+
+DEFINE_bool(use_ribbon_filter, false, "Use Ribbon instead of Bloom filter");
+
+DEFINE_double(memtable_bloom_size_ratio, 0,
+ "Ratio of memtable size used for bloom filter. 0 means no bloom "
+ "filter.");
+DEFINE_bool(memtable_whole_key_filtering, false,
+ "Try to use whole key bloom filter in memtables.");
+DEFINE_bool(memtable_use_huge_page, false,
+ "Try to use huge page in memtables.");
+
+DEFINE_bool(whole_key_filtering,
+ ROCKSDB_NAMESPACE::BlockBasedTableOptions().whole_key_filtering,
+ "Use whole keys (in addition to prefixes) in SST bloom filter.");
+
+DEFINE_bool(use_existing_db, false,
+ "If true, do not destroy the existing database. If you set this "
+ "flag and also specify a benchmark that wants a fresh database, "
+ "that benchmark will fail.");
+
+DEFINE_bool(use_existing_keys, false,
+ "If true, uses existing keys in the DB, "
+ "rather than generating new ones. This involves some startup "
+ "latency to load all keys into memory. It is supported for the "
+ "same read/overwrite benchmarks as `-use_existing_db=true`, which "
+ "must also be set for this flag to be enabled. When this flag is "
+ "set, the value for `-num` will be ignored.");
+
+DEFINE_bool(show_table_properties, false,
+ "If true, then per-level table"
+ " properties will be printed on every stats-interval when"
+ " stats_interval is set and stats_per_interval is on.");
+
+DEFINE_string(db, "", "Use the db with the following name.");
+
+DEFINE_bool(progress_reports, true,
+ "If true, db_bench will report number of finished operations.");
+
+// Read cache flags
+
+DEFINE_string(read_cache_path, "",
+ "If not empty string, a read cache will be used in this path");
+
+DEFINE_int64(read_cache_size, 4LL * 1024 * 1024 * 1024,
+ "Maximum size of the read cache");
+
+DEFINE_bool(read_cache_direct_write, true,
+ "Whether to use Direct IO for writing to the read cache");
+
+DEFINE_bool(read_cache_direct_read, true,
+ "Whether to use Direct IO for reading from read cache");
+
+DEFINE_bool(use_keep_filter, false, "Whether to use a noop compaction filter");
+
+static bool ValidateCacheNumshardbits(const char* flagname, int32_t value) {
+ if (value >= 20) {
+ fprintf(stderr, "Invalid value for --%s: %d, must be < 20\n", flagname,
+ value);
+ return false;
+ }
+ return true;
+}
+
+DEFINE_bool(verify_checksum, true,
+ "Verify checksum for every block read from storage");
+
+DEFINE_int32(checksum_type,
+ ROCKSDB_NAMESPACE::BlockBasedTableOptions().checksum,
+ "ChecksumType as an int");
+
+DEFINE_bool(statistics, false, "Database statistics");
+DEFINE_int32(stats_level, ROCKSDB_NAMESPACE::StatsLevel::kExceptDetailedTimers,
+ "stats level for statistics");
+DEFINE_string(statistics_string, "", "Serialized statistics string");
+static class std::shared_ptr<ROCKSDB_NAMESPACE::Statistics> dbstats;
+
+DEFINE_int64(writes, -1,
+ "Number of write operations to do. If negative, do --num reads.");
+
+DEFINE_bool(finish_after_writes, false,
+ "Write thread terminates after all writes are finished");
+
+DEFINE_bool(sync, false, "Sync all writes to disk");
+
+DEFINE_bool(use_fsync, false, "If true, issue fsync instead of fdatasync");
+
+DEFINE_bool(disable_wal, false, "If true, do not write WAL for write.");
+
+DEFINE_bool(manual_wal_flush, false,
+ "If true, buffer WAL until buffer is full or a manual FlushWAL().");
+
+DEFINE_string(wal_compression, "none",
+ "Algorithm to use for WAL compression. none to disable.");
+static enum ROCKSDB_NAMESPACE::CompressionType FLAGS_wal_compression_e =
+ ROCKSDB_NAMESPACE::kNoCompression;
+
+DEFINE_string(wal_dir, "", "If not empty, use the given dir for WAL");
+
+DEFINE_string(truth_db, "/dev/shm/truth_db/dbbench",
+ "Truth key/values used when using verify");
+
+DEFINE_int32(num_levels, 7, "The total number of levels");
+
+DEFINE_int64(target_file_size_base,
+ ROCKSDB_NAMESPACE::Options().target_file_size_base,
+ "Target file size at level-1");
+
+DEFINE_int32(target_file_size_multiplier,
+ ROCKSDB_NAMESPACE::Options().target_file_size_multiplier,
+ "A multiplier to compute target level-N file size (N >= 2)");
+
+DEFINE_uint64(max_bytes_for_level_base,
+ ROCKSDB_NAMESPACE::Options().max_bytes_for_level_base,
+ "Max bytes for level-1");
+
+DEFINE_bool(level_compaction_dynamic_level_bytes, false,
+ "Whether level size base is dynamic");
+
+DEFINE_double(max_bytes_for_level_multiplier, 10,
+ "A multiplier to compute max bytes for level-N (N >= 2)");
+
+static std::vector<int> FLAGS_max_bytes_for_level_multiplier_additional_v;
+DEFINE_string(max_bytes_for_level_multiplier_additional, "",
+ "A vector that specifies additional fanout per level");
+
+DEFINE_int32(level0_stop_writes_trigger,
+ ROCKSDB_NAMESPACE::Options().level0_stop_writes_trigger,
+ "Number of files in level-0 that will trigger put stop.");
+
+DEFINE_int32(level0_slowdown_writes_trigger,
+ ROCKSDB_NAMESPACE::Options().level0_slowdown_writes_trigger,
+ "Number of files in level-0 that will slow down writes.");
+
+DEFINE_int32(level0_file_num_compaction_trigger,
+ ROCKSDB_NAMESPACE::Options().level0_file_num_compaction_trigger,
+ "Number of files in level-0 when compactions start.");
+
+DEFINE_uint64(periodic_compaction_seconds,
+ ROCKSDB_NAMESPACE::Options().periodic_compaction_seconds,
+ "Files older than this will be picked up for compaction and"
+ " rewritten to the same level");
+
+DEFINE_uint64(ttl_seconds, ROCKSDB_NAMESPACE::Options().ttl, "Set options.ttl");
+
+static bool ValidateInt32Percent(const char* flagname, int32_t value) {
+ if (value <= 0 || value >= 100) {
+ fprintf(stderr, "Invalid value for --%s: %d, 0< pct <100 \n", flagname,
+ value);
+ return false;
+ }
+ return true;
+}
+DEFINE_int32(readwritepercent, 90,
+ "Ratio of reads to reads/writes (expressed as percentage) for "
+ "the ReadRandomWriteRandom workload. The default value 90 means "
+ "90% operations out of all reads and writes operations are "
+ "reads. In other words, 9 gets for every 1 put.");
+
+DEFINE_int32(mergereadpercent, 70,
+ "Ratio of merges to merges&reads (expressed as percentage) for "
+ "the ReadRandomMergeRandom workload. The default value 70 means "
+ "70% out of all read and merge operations are merges. In other "
+ "words, 7 merges for every 3 gets.");
+
+DEFINE_int32(deletepercent, 2,
+ "Percentage of deletes out of reads/writes/deletes (used in "
+ "RandomWithVerify only). RandomWithVerify "
+ "calculates writepercent as (100 - FLAGS_readwritepercent - "
+ "deletepercent), so deletepercent must be smaller than (100 - "
+ "FLAGS_readwritepercent)");
+
+DEFINE_bool(optimize_filters_for_hits,
+ ROCKSDB_NAMESPACE::Options().optimize_filters_for_hits,
+ "Optimizes bloom filters for workloads for most lookups return "
+ "a value. For now this doesn't create bloom filters for the max "
+ "level of the LSM to reduce metadata that should fit in RAM. ");
+
+DEFINE_bool(paranoid_checks, ROCKSDB_NAMESPACE::Options().paranoid_checks,
+ "RocksDB will aggressively check consistency of the data.");
+
+DEFINE_bool(force_consistency_checks,
+ ROCKSDB_NAMESPACE::Options().force_consistency_checks,
+ "Runs consistency checks on the LSM every time a change is "
+ "applied.");
+
+DEFINE_bool(check_flush_compaction_key_order,
+ ROCKSDB_NAMESPACE::Options().check_flush_compaction_key_order,
+ "During flush or compaction, check whether keys inserted to "
+ "output files are in order.");
+
+DEFINE_uint64(delete_obsolete_files_period_micros, 0,
+ "Ignored. Left here for backward compatibility");
+
+DEFINE_int64(writes_before_delete_range, 0,
+ "Number of writes before DeleteRange is called regularly.");
+
+DEFINE_int64(writes_per_range_tombstone, 0,
+ "Number of writes between range tombstones");
+
+DEFINE_int64(range_tombstone_width, 100, "Number of keys in tombstone's range");
+
+DEFINE_int64(max_num_range_tombstones, 0,
+ "Maximum number of range tombstones to insert.");
+
+DEFINE_bool(expand_range_tombstones, false,
+ "Expand range tombstone into sequential regular tombstones.");
+
+#ifndef ROCKSDB_LITE
+// Transactions Options
+DEFINE_bool(optimistic_transaction_db, false,
+ "Open a OptimisticTransactionDB instance. "
+ "Required for randomtransaction benchmark.");
+
+DEFINE_bool(transaction_db, false,
+ "Open a TransactionDB instance. "
+ "Required for randomtransaction benchmark.");
+
+DEFINE_uint64(transaction_sets, 2,
+ "Number of keys each transaction will "
+ "modify (use in RandomTransaction only). Max: 9999");
+
+DEFINE_bool(transaction_set_snapshot, false,
+ "Setting to true will have each transaction call SetSnapshot()"
+ " upon creation.");
+
+DEFINE_int32(transaction_sleep, 0,
+ "Max microseconds to sleep in between "
+ "reading and writing a value (used in RandomTransaction only). ");
+
+DEFINE_uint64(transaction_lock_timeout, 100,
+ "If using a transaction_db, specifies the lock wait timeout in"
+ " milliseconds before failing a transaction waiting on a lock");
+DEFINE_string(
+ options_file, "",
+ "The path to a RocksDB options file. If specified, then db_bench will "
+ "run with the RocksDB options in the default column family of the "
+ "specified options file. "
+ "Note that with this setting, db_bench will ONLY accept the following "
+ "RocksDB options related command-line arguments, all other arguments "
+ "that are related to RocksDB options will be ignored:\n"
+ "\t--use_existing_db\n"
+ "\t--use_existing_keys\n"
+ "\t--statistics\n"
+ "\t--row_cache_size\n"
+ "\t--row_cache_numshardbits\n"
+ "\t--enable_io_prio\n"
+ "\t--dump_malloc_stats\n"
+ "\t--num_multi_db\n");
+
+// FIFO Compaction Options
+DEFINE_uint64(fifo_compaction_max_table_files_size_mb, 0,
+ "The limit of total table file sizes to trigger FIFO compaction");
+
+DEFINE_bool(fifo_compaction_allow_compaction, true,
+ "Allow compaction in FIFO compaction.");
+
+DEFINE_uint64(fifo_compaction_ttl, 0, "TTL for the SST Files in seconds.");
+
+DEFINE_uint64(fifo_age_for_warm, 0, "age_for_warm for FIFO compaction.");
+
+// Stacked BlobDB Options
+DEFINE_bool(use_blob_db, false, "[Stacked BlobDB] Open a BlobDB instance.");
+
+DEFINE_bool(
+ blob_db_enable_gc,
+ ROCKSDB_NAMESPACE::blob_db::BlobDBOptions().enable_garbage_collection,
+ "[Stacked BlobDB] Enable BlobDB garbage collection.");
+
+DEFINE_double(
+ blob_db_gc_cutoff,
+ ROCKSDB_NAMESPACE::blob_db::BlobDBOptions().garbage_collection_cutoff,
+ "[Stacked BlobDB] Cutoff ratio for BlobDB garbage collection.");
+
+DEFINE_bool(blob_db_is_fifo,
+ ROCKSDB_NAMESPACE::blob_db::BlobDBOptions().is_fifo,
+ "[Stacked BlobDB] Enable FIFO eviction strategy in BlobDB.");
+
+DEFINE_uint64(blob_db_max_db_size,
+ ROCKSDB_NAMESPACE::blob_db::BlobDBOptions().max_db_size,
+ "[Stacked BlobDB] Max size limit of the directory where blob "
+ "files are stored.");
+
+DEFINE_uint64(blob_db_max_ttl_range, 0,
+ "[Stacked BlobDB] TTL range to generate BlobDB data (in "
+ "seconds). 0 means no TTL.");
+
+DEFINE_uint64(
+ blob_db_ttl_range_secs,
+ ROCKSDB_NAMESPACE::blob_db::BlobDBOptions().ttl_range_secs,
+ "[Stacked BlobDB] TTL bucket size to use when creating blob files.");
+
+DEFINE_uint64(
+ blob_db_min_blob_size,
+ ROCKSDB_NAMESPACE::blob_db::BlobDBOptions().min_blob_size,
+ "[Stacked BlobDB] Smallest blob to store in a file. Blobs "
+ "smaller than this will be inlined with the key in the LSM tree.");
+
+DEFINE_uint64(blob_db_bytes_per_sync,
+ ROCKSDB_NAMESPACE::blob_db::BlobDBOptions().bytes_per_sync,
+ "[Stacked BlobDB] Bytes to sync blob file at.");
+
+DEFINE_uint64(blob_db_file_size,
+ ROCKSDB_NAMESPACE::blob_db::BlobDBOptions().blob_file_size,
+ "[Stacked BlobDB] Target size of each blob file.");
+
+DEFINE_string(
+ blob_db_compression_type, "snappy",
+ "[Stacked BlobDB] Algorithm to use to compress blobs in blob files.");
+static enum ROCKSDB_NAMESPACE::CompressionType
+ FLAGS_blob_db_compression_type_e = ROCKSDB_NAMESPACE::kSnappyCompression;
+
+#endif // ROCKSDB_LITE
+
+// Integrated BlobDB options
+DEFINE_bool(
+ enable_blob_files,
+ ROCKSDB_NAMESPACE::AdvancedColumnFamilyOptions().enable_blob_files,
+ "[Integrated BlobDB] Enable writing large values to separate blob files.");
+
+DEFINE_uint64(min_blob_size,
+ ROCKSDB_NAMESPACE::AdvancedColumnFamilyOptions().min_blob_size,
+ "[Integrated BlobDB] The size of the smallest value to be stored "
+ "separately in a blob file.");
+
+DEFINE_uint64(blob_file_size,
+ ROCKSDB_NAMESPACE::AdvancedColumnFamilyOptions().blob_file_size,
+ "[Integrated BlobDB] The size limit for blob files.");
+
+DEFINE_string(blob_compression_type, "none",
+ "[Integrated BlobDB] The compression algorithm to use for large "
+ "values stored in blob files.");
+
+DEFINE_bool(enable_blob_garbage_collection,
+ ROCKSDB_NAMESPACE::AdvancedColumnFamilyOptions()
+ .enable_blob_garbage_collection,
+ "[Integrated BlobDB] Enable blob garbage collection.");
+
+DEFINE_double(blob_garbage_collection_age_cutoff,
+ ROCKSDB_NAMESPACE::AdvancedColumnFamilyOptions()
+ .blob_garbage_collection_age_cutoff,
+ "[Integrated BlobDB] The cutoff in terms of blob file age for "
+ "garbage collection.");
+
+DEFINE_double(blob_garbage_collection_force_threshold,
+ ROCKSDB_NAMESPACE::AdvancedColumnFamilyOptions()
+ .blob_garbage_collection_force_threshold,
+ "[Integrated BlobDB] The threshold for the ratio of garbage in "
+ "the oldest blob files for forcing garbage collection.");
+
+DEFINE_uint64(blob_compaction_readahead_size,
+ ROCKSDB_NAMESPACE::AdvancedColumnFamilyOptions()
+ .blob_compaction_readahead_size,
+ "[Integrated BlobDB] Compaction readahead for blob files.");
+
+DEFINE_int32(
+ blob_file_starting_level,
+ ROCKSDB_NAMESPACE::AdvancedColumnFamilyOptions().blob_file_starting_level,
+ "[Integrated BlobDB] The starting level for blob files.");
+
+DEFINE_bool(use_blob_cache, false, "[Integrated BlobDB] Enable blob cache.");
+
+DEFINE_bool(
+ use_shared_block_and_blob_cache, true,
+ "[Integrated BlobDB] Use a shared backing cache for both block "
+ "cache and blob cache. It only takes effect if use_blob_cache is enabled.");
+
+DEFINE_uint64(
+ blob_cache_size, 8 << 20,
+ "[Integrated BlobDB] Number of bytes to use as a cache of blobs. It only "
+ "takes effect if the block and blob caches are different "
+ "(use_shared_block_and_blob_cache = false).");
+
+DEFINE_int32(blob_cache_numshardbits, 6,
+ "[Integrated BlobDB] Number of shards for the blob cache is 2 ** "
+ "blob_cache_numshardbits. Negative means use default settings. "
+ "It only takes effect if blob_cache_size is greater than 0, and "
+ "the block and blob caches are different "
+ "(use_shared_block_and_blob_cache = false).");
+
+DEFINE_int32(prepopulate_blob_cache, 0,
+ "[Integrated BlobDB] Pre-populate hot/warm blobs in blob cache. 0 "
+ "to disable and 1 to insert during flush.");
+
+#ifndef ROCKSDB_LITE
+
+// Secondary DB instance Options
+DEFINE_bool(use_secondary_db, false,
+ "Open a RocksDB secondary instance. A primary instance can be "
+ "running in another db_bench process.");
+
+DEFINE_string(secondary_path, "",
+ "Path to a directory used by the secondary instance to store "
+ "private files, e.g. info log.");
+
+DEFINE_int32(secondary_update_interval, 5,
+ "Secondary instance attempts to catch up with the primary every "
+ "secondary_update_interval seconds.");
+
+#endif // ROCKSDB_LITE
+
+DEFINE_bool(report_bg_io_stats, false,
+ "Measure times spents on I/Os while in compactions. ");
+
+DEFINE_bool(use_stderr_info_logger, false,
+ "Write info logs to stderr instead of to LOG file. ");
+
+#ifndef ROCKSDB_LITE
+
+DEFINE_string(trace_file, "", "Trace workload to a file. ");
+
+DEFINE_double(trace_replay_fast_forward, 1.0,
+ "Fast forward trace replay, must > 0.0.");
+DEFINE_int32(block_cache_trace_sampling_frequency, 1,
+ "Block cache trace sampling frequency, termed s. It uses spatial "
+ "downsampling and samples accesses to one out of s blocks.");
+DEFINE_int64(
+ block_cache_trace_max_trace_file_size_in_bytes,
+ uint64_t{64} * 1024 * 1024 * 1024,
+ "The maximum block cache trace file size in bytes. Block cache accesses "
+ "will not be logged if the trace file size exceeds this threshold. Default "
+ "is 64 GB.");
+DEFINE_string(block_cache_trace_file, "", "Block cache trace file path.");
+DEFINE_int32(trace_replay_threads, 1,
+ "The number of threads to replay, must >=1.");
+
+DEFINE_bool(io_uring_enabled, true,
+ "If true, enable the use of IO uring if the platform supports it");
+extern "C" bool RocksDbIOUringEnable() { return FLAGS_io_uring_enabled; }
+#endif // ROCKSDB_LITE
+
+DEFINE_bool(adaptive_readahead, false,
+ "carry forward internal auto readahead size from one file to next "
+ "file at each level during iteration");
+
+DEFINE_bool(rate_limit_user_ops, false,
+ "When true use Env::IO_USER priority level to charge internal rate "
+ "limiter for reads associated with user operations.");
+
+DEFINE_bool(file_checksum, false,
+ "When true use FileChecksumGenCrc32cFactory for "
+ "file_checksum_gen_factory.");
+
+DEFINE_bool(rate_limit_auto_wal_flush, false,
+ "When true use Env::IO_USER priority level to charge internal rate "
+ "limiter for automatic WAL flush (`Options::manual_wal_flush` == "
+ "false) after the user write operation.");
+
+DEFINE_bool(async_io, false,
+ "When set true, RocksDB does asynchronous reads for internal auto "
+ "readahead prefetching.");
+
+DEFINE_bool(optimize_multiget_for_io, true,
+ "When set true, RocksDB does asynchronous reads for SST files in "
+ "multiple levels for MultiGet.");
+
+DEFINE_bool(charge_compression_dictionary_building_buffer, false,
+ "Setting for "
+ "CacheEntryRoleOptions::charged of "
+ "CacheEntryRole::kCompressionDictionaryBuildingBuffer");
+
+DEFINE_bool(charge_filter_construction, false,
+ "Setting for "
+ "CacheEntryRoleOptions::charged of "
+ "CacheEntryRole::kFilterConstruction");
+
+DEFINE_bool(charge_table_reader, false,
+ "Setting for "
+ "CacheEntryRoleOptions::charged of "
+ "CacheEntryRole::kBlockBasedTableReader");
+
+DEFINE_bool(charge_file_metadata, false,
+ "Setting for "
+ "CacheEntryRoleOptions::charged of "
+ "CacheEntryRole::kFileMetadata");
+
+DEFINE_bool(charge_blob_cache, false,
+ "Setting for "
+ "CacheEntryRoleOptions::charged of "
+ "CacheEntryRole::kBlobCache");
+
+DEFINE_uint64(backup_rate_limit, 0ull,
+ "If non-zero, db_bench will rate limit reads and writes for DB "
+ "backup. This "
+ "is the global rate in ops/second.");
+
+DEFINE_uint64(restore_rate_limit, 0ull,
+ "If non-zero, db_bench will rate limit reads and writes for DB "
+ "restore. This "
+ "is the global rate in ops/second.");
+
+DEFINE_string(backup_dir, "",
+ "If not empty string, use the given dir for backup.");
+
+DEFINE_string(restore_dir, "",
+ "If not empty string, use the given dir for restore.");
+
+DEFINE_uint64(
+ initial_auto_readahead_size,
+ ROCKSDB_NAMESPACE::BlockBasedTableOptions().initial_auto_readahead_size,
+ "RocksDB does auto-readahead for iterators on noticing more than two reads "
+ "for a table file if user doesn't provide readahead_size. The readahead "
+ "size starts at initial_auto_readahead_size");
+
+DEFINE_uint64(
+ max_auto_readahead_size,
+ ROCKSDB_NAMESPACE::BlockBasedTableOptions().max_auto_readahead_size,
+ "Rocksdb implicit readahead starts at "
+ "BlockBasedTableOptions.initial_auto_readahead_size and doubles on every "
+ "additional read upto max_auto_readahead_size");
+
+DEFINE_uint64(
+ num_file_reads_for_auto_readahead,
+ ROCKSDB_NAMESPACE::BlockBasedTableOptions()
+ .num_file_reads_for_auto_readahead,
+ "Rocksdb implicit readahead is enabled if reads are sequential and "
+ "num_file_reads_for_auto_readahead indicates after how many sequential "
+ "reads into that file internal auto prefetching should be start.");
+
+static enum ROCKSDB_NAMESPACE::CompressionType StringToCompressionType(
+ const char* ctype) {
+ assert(ctype);
+
+ if (!strcasecmp(ctype, "none"))
+ return ROCKSDB_NAMESPACE::kNoCompression;
+ else if (!strcasecmp(ctype, "snappy"))
+ return ROCKSDB_NAMESPACE::kSnappyCompression;
+ else if (!strcasecmp(ctype, "zlib"))
+ return ROCKSDB_NAMESPACE::kZlibCompression;
+ else if (!strcasecmp(ctype, "bzip2"))
+ return ROCKSDB_NAMESPACE::kBZip2Compression;
+ else if (!strcasecmp(ctype, "lz4"))
+ return ROCKSDB_NAMESPACE::kLZ4Compression;
+ else if (!strcasecmp(ctype, "lz4hc"))
+ return ROCKSDB_NAMESPACE::kLZ4HCCompression;
+ else if (!strcasecmp(ctype, "xpress"))
+ return ROCKSDB_NAMESPACE::kXpressCompression;
+ else if (!strcasecmp(ctype, "zstd"))
+ return ROCKSDB_NAMESPACE::kZSTD;
+ else {
+ fprintf(stderr, "Cannot parse compression type '%s'\n", ctype);
+ exit(1);
+ }
+}
+
+static std::string ColumnFamilyName(size_t i) {
+ if (i == 0) {
+ return ROCKSDB_NAMESPACE::kDefaultColumnFamilyName;
+ } else {
+ char name[100];
+ snprintf(name, sizeof(name), "column_family_name_%06zu", i);
+ return std::string(name);
+ }
+}
+
+DEFINE_string(compression_type, "snappy",
+ "Algorithm to use to compress the database");
+static enum ROCKSDB_NAMESPACE::CompressionType FLAGS_compression_type_e =
+ ROCKSDB_NAMESPACE::kSnappyCompression;
+
+DEFINE_int64(sample_for_compression, 0, "Sample every N block for compression");
+
+DEFINE_int32(compression_level, ROCKSDB_NAMESPACE::CompressionOptions().level,
+ "Compression level. The meaning of this value is library-"
+ "dependent. If unset, we try to use the default for the library "
+ "specified in `--compression_type`");
+
+DEFINE_int32(compression_max_dict_bytes,
+ ROCKSDB_NAMESPACE::CompressionOptions().max_dict_bytes,
+ "Maximum size of dictionary used to prime the compression "
+ "library.");
+
+DEFINE_int32(compression_zstd_max_train_bytes,
+ ROCKSDB_NAMESPACE::CompressionOptions().zstd_max_train_bytes,
+ "Maximum size of training data passed to zstd's dictionary "
+ "trainer.");
+
+DEFINE_int32(min_level_to_compress, -1,
+ "If non-negative, compression starts"
+ " from this level. Levels with number < min_level_to_compress are"
+ " not compressed. Otherwise, apply compression_type to "
+ "all levels.");
+
+DEFINE_int32(compression_parallel_threads, 1,
+ "Number of threads for parallel compression.");
+
+DEFINE_uint64(compression_max_dict_buffer_bytes,
+ ROCKSDB_NAMESPACE::CompressionOptions().max_dict_buffer_bytes,
+ "Maximum bytes to buffer to collect samples for dictionary.");
+
+DEFINE_bool(compression_use_zstd_dict_trainer,
+ ROCKSDB_NAMESPACE::CompressionOptions().use_zstd_dict_trainer,
+ "If true, use ZSTD_TrainDictionary() to create dictionary, else"
+ "use ZSTD_FinalizeDictionary() to create dictionary");
+
+static bool ValidateTableCacheNumshardbits(const char* flagname,
+ int32_t value) {
+ if (0 >= value || value >= 20) {
+ fprintf(stderr, "Invalid value for --%s: %d, must be 0 < val < 20\n",
+ flagname, value);
+ return false;
+ }
+ return true;
+}
+DEFINE_int32(table_cache_numshardbits, 4, "");
+
+#ifndef ROCKSDB_LITE
+DEFINE_string(env_uri, "",
+ "URI for registry Env lookup. Mutually exclusive with --fs_uri");
+DEFINE_string(fs_uri, "",
+ "URI for registry Filesystem lookup. Mutually exclusive"
+ " with --env_uri."
+ " Creates a default environment with the specified filesystem.");
+#endif // ROCKSDB_LITE
+DEFINE_string(simulate_hybrid_fs_file, "",
+ "File for Store Metadata for Simulate hybrid FS. Empty means "
+ "disable the feature. Now, if it is set, last_level_temperature "
+ "is set to kWarm.");
+DEFINE_int32(simulate_hybrid_hdd_multipliers, 1,
+ "In simulate_hybrid_fs_file or simulate_hdd mode, how many HDDs "
+ "are simulated.");
+DEFINE_bool(simulate_hdd, false, "Simulate read/write latency on HDD.");
+
+DEFINE_int64(
+ preclude_last_level_data_seconds, 0,
+ "Preclude the latest data from the last level. (Used for tiered storage)");
+
+DEFINE_int64(preserve_internal_time_seconds, 0,
+ "Preserve the internal time information which stores with SST.");
+
+static std::shared_ptr<ROCKSDB_NAMESPACE::Env> env_guard;
+
+static ROCKSDB_NAMESPACE::Env* FLAGS_env = ROCKSDB_NAMESPACE::Env::Default();
+
+DEFINE_int64(stats_interval, 0,
+ "Stats are reported every N operations when this is greater than "
+ "zero. When 0 the interval grows over time.");
+
+DEFINE_int64(stats_interval_seconds, 0,
+ "Report stats every N seconds. This overrides stats_interval when"
+ " both are > 0.");
+
+DEFINE_int32(stats_per_interval, 0,
+ "Reports additional stats per interval when this is greater than "
+ "0.");
+
+DEFINE_uint64(slow_usecs, 1000000,
+ "A message is printed for operations that take at least this "
+ "many microseconds.");
+
+DEFINE_int64(report_interval_seconds, 0,
+ "If greater than zero, it will write simple stats in CSV format "
+ "to --report_file every N seconds");
+
+DEFINE_string(report_file, "report.csv",
+ "Filename where some simple stats are reported to (if "
+ "--report_interval_seconds is bigger than 0)");
+
+DEFINE_int32(thread_status_per_interval, 0,
+ "Takes and report a snapshot of the current status of each thread"
+ " when this is greater than 0.");
+
+DEFINE_int32(perf_level, ROCKSDB_NAMESPACE::PerfLevel::kDisable,
+ "Level of perf collection");
+
+DEFINE_uint64(soft_pending_compaction_bytes_limit, 64ull * 1024 * 1024 * 1024,
+ "Slowdown writes if pending compaction bytes exceed this number");
+
+DEFINE_uint64(hard_pending_compaction_bytes_limit, 128ull * 1024 * 1024 * 1024,
+ "Stop writes if pending compaction bytes exceed this number");
+
+DEFINE_uint64(delayed_write_rate, 8388608u,
+ "Limited bytes allowed to DB when soft_rate_limit or "
+ "level0_slowdown_writes_trigger triggers");
+
+DEFINE_bool(enable_pipelined_write, true,
+ "Allow WAL and memtable writes to be pipelined");
+
+DEFINE_bool(
+ unordered_write, false,
+ "Enable the unordered write feature, which provides higher throughput but "
+ "relaxes the guarantees around atomic reads and immutable snapshots");
+
+DEFINE_bool(allow_concurrent_memtable_write, true,
+ "Allow multi-writers to update mem tables in parallel.");
+
+DEFINE_double(experimental_mempurge_threshold, 0.0,
+ "Maximum useful payload ratio estimate that triggers a mempurge "
+ "(memtable garbage collection).");
+
+DEFINE_bool(inplace_update_support,
+ ROCKSDB_NAMESPACE::Options().inplace_update_support,
+ "Support in-place memtable update for smaller or same-size values");
+
+DEFINE_uint64(inplace_update_num_locks,
+ ROCKSDB_NAMESPACE::Options().inplace_update_num_locks,
+ "Number of RW locks to protect in-place memtable updates");
+
+DEFINE_bool(enable_write_thread_adaptive_yield, true,
+ "Use a yielding spin loop for brief writer thread waits.");
+
+DEFINE_uint64(
+ write_thread_max_yield_usec, 100,
+ "Maximum microseconds for enable_write_thread_adaptive_yield operation.");
+
+DEFINE_uint64(write_thread_slow_yield_usec, 3,
+ "The threshold at which a slow yield is considered a signal that "
+ "other processes or threads want the core.");
+
+DEFINE_uint64(rate_limiter_bytes_per_sec, 0, "Set options.rate_limiter value.");
+
+DEFINE_int64(rate_limiter_refill_period_us, 100 * 1000,
+ "Set refill period on rate limiter.");
+
+DEFINE_bool(rate_limiter_auto_tuned, false,
+ "Enable dynamic adjustment of rate limit according to demand for "
+ "background I/O");
+
+DEFINE_bool(sine_write_rate, false, "Use a sine wave write_rate_limit");
+
+DEFINE_uint64(
+ sine_write_rate_interval_milliseconds, 10000,
+ "Interval of which the sine wave write_rate_limit is recalculated");
+
+DEFINE_double(sine_a, 1, "A in f(x) = A sin(bx + c) + d");
+
+DEFINE_double(sine_b, 1, "B in f(x) = A sin(bx + c) + d");
+
+DEFINE_double(sine_c, 0, "C in f(x) = A sin(bx + c) + d");
+
+DEFINE_double(sine_d, 1, "D in f(x) = A sin(bx + c) + d");
+
+DEFINE_bool(rate_limit_bg_reads, false,
+ "Use options.rate_limiter on compaction reads");
+
+DEFINE_uint64(
+ benchmark_write_rate_limit, 0,
+ "If non-zero, db_bench will rate-limit the writes going into RocksDB. This "
+ "is the global rate in bytes/second.");
+
+// the parameters of mix_graph
+DEFINE_double(keyrange_dist_a, 0.0,
+ "The parameter 'a' of prefix average access distribution "
+ "f(x)=a*exp(b*x)+c*exp(d*x)");
+DEFINE_double(keyrange_dist_b, 0.0,
+ "The parameter 'b' of prefix average access distribution "
+ "f(x)=a*exp(b*x)+c*exp(d*x)");
+DEFINE_double(keyrange_dist_c, 0.0,
+ "The parameter 'c' of prefix average access distribution"
+ "f(x)=a*exp(b*x)+c*exp(d*x)");
+DEFINE_double(keyrange_dist_d, 0.0,
+ "The parameter 'd' of prefix average access distribution"
+ "f(x)=a*exp(b*x)+c*exp(d*x)");
+DEFINE_int64(keyrange_num, 1,
+ "The number of key ranges that are in the same prefix "
+ "group, each prefix range will have its key access distribution");
+DEFINE_double(key_dist_a, 0.0,
+ "The parameter 'a' of key access distribution model f(x)=a*x^b");
+DEFINE_double(key_dist_b, 0.0,
+ "The parameter 'b' of key access distribution model f(x)=a*x^b");
+DEFINE_double(value_theta, 0.0,
+ "The parameter 'theta' of Generized Pareto Distribution "
+ "f(x)=(1/sigma)*(1+k*(x-theta)/sigma)^-(1/k+1)");
+// Use reasonable defaults based on the mixgraph paper
+DEFINE_double(value_k, 0.2615,
+ "The parameter 'k' of Generized Pareto Distribution "
+ "f(x)=(1/sigma)*(1+k*(x-theta)/sigma)^-(1/k+1)");
+// Use reasonable defaults based on the mixgraph paper
+DEFINE_double(value_sigma, 25.45,
+ "The parameter 'theta' of Generized Pareto Distribution "
+ "f(x)=(1/sigma)*(1+k*(x-theta)/sigma)^-(1/k+1)");
+DEFINE_double(iter_theta, 0.0,
+ "The parameter 'theta' of Generized Pareto Distribution "
+ "f(x)=(1/sigma)*(1+k*(x-theta)/sigma)^-(1/k+1)");
+// Use reasonable defaults based on the mixgraph paper
+DEFINE_double(iter_k, 2.517,
+ "The parameter 'k' of Generized Pareto Distribution "
+ "f(x)=(1/sigma)*(1+k*(x-theta)/sigma)^-(1/k+1)");
+// Use reasonable defaults based on the mixgraph paper
+DEFINE_double(iter_sigma, 14.236,
+ "The parameter 'sigma' of Generized Pareto Distribution "
+ "f(x)=(1/sigma)*(1+k*(x-theta)/sigma)^-(1/k+1)");
+DEFINE_double(mix_get_ratio, 1.0,
+ "The ratio of Get queries of mix_graph workload");
+DEFINE_double(mix_put_ratio, 0.0,
+ "The ratio of Put queries of mix_graph workload");
+DEFINE_double(mix_seek_ratio, 0.0,
+ "The ratio of Seek queries of mix_graph workload");
+DEFINE_int64(mix_max_scan_len, 10000, "The max scan length of Iterator");
+DEFINE_int64(mix_max_value_size, 1024, "The max value size of this workload");
+DEFINE_double(
+ sine_mix_rate_noise, 0.0,
+ "Add the noise ratio to the sine rate, it is between 0.0 and 1.0");
+DEFINE_bool(sine_mix_rate, false,
+ "Enable the sine QPS control on the mix workload");
+DEFINE_uint64(
+ sine_mix_rate_interval_milliseconds, 10000,
+ "Interval of which the sine wave read_rate_limit is recalculated");
+DEFINE_int64(mix_accesses, -1,
+ "The total query accesses of mix_graph workload");
+
+DEFINE_uint64(
+ benchmark_read_rate_limit, 0,
+ "If non-zero, db_bench will rate-limit the reads from RocksDB. This "
+ "is the global rate in ops/second.");
+
+DEFINE_uint64(max_compaction_bytes,
+ ROCKSDB_NAMESPACE::Options().max_compaction_bytes,
+ "Max bytes allowed in one compaction");
+
+#ifndef ROCKSDB_LITE
+DEFINE_bool(readonly, false, "Run read only benchmarks.");
+
+DEFINE_bool(print_malloc_stats, false,
+ "Print malloc stats to stdout after benchmarks finish.");
+#endif // ROCKSDB_LITE
+
+DEFINE_bool(disable_auto_compactions, false, "Do not auto trigger compactions");
+
+DEFINE_uint64(wal_ttl_seconds, 0, "Set the TTL for the WAL Files in seconds.");
+DEFINE_uint64(wal_size_limit_MB, 0,
+ "Set the size limit for the WAL Files in MB.");
+DEFINE_uint64(max_total_wal_size, 0, "Set total max WAL size");
+
+DEFINE_bool(mmap_read, ROCKSDB_NAMESPACE::Options().allow_mmap_reads,
+ "Allow reads to occur via mmap-ing files");
+
+DEFINE_bool(mmap_write, ROCKSDB_NAMESPACE::Options().allow_mmap_writes,
+ "Allow writes to occur via mmap-ing files");
+
+DEFINE_bool(use_direct_reads, ROCKSDB_NAMESPACE::Options().use_direct_reads,
+ "Use O_DIRECT for reading data");
+
+DEFINE_bool(use_direct_io_for_flush_and_compaction,
+ ROCKSDB_NAMESPACE::Options().use_direct_io_for_flush_and_compaction,
+ "Use O_DIRECT for background flush and compaction writes");
+
+DEFINE_bool(advise_random_on_open,
+ ROCKSDB_NAMESPACE::Options().advise_random_on_open,
+ "Advise random access on table file open");
+
+DEFINE_string(compaction_fadvice, "NORMAL",
+ "Access pattern advice when a file is compacted");
+static auto FLAGS_compaction_fadvice_e =
+ ROCKSDB_NAMESPACE::Options().access_hint_on_compaction_start;
+
+DEFINE_bool(use_tailing_iterator, false,
+ "Use tailing iterator to access a series of keys instead of get");
+
+DEFINE_bool(use_adaptive_mutex, ROCKSDB_NAMESPACE::Options().use_adaptive_mutex,
+ "Use adaptive mutex");
+
+DEFINE_uint64(bytes_per_sync, ROCKSDB_NAMESPACE::Options().bytes_per_sync,
+ "Allows OS to incrementally sync SST files to disk while they are"
+ " being written, in the background. Issue one request for every"
+ " bytes_per_sync written. 0 turns it off.");
+
+DEFINE_uint64(wal_bytes_per_sync,
+ ROCKSDB_NAMESPACE::Options().wal_bytes_per_sync,
+ "Allows OS to incrementally sync WAL files to disk while they are"
+ " being written, in the background. Issue one request for every"
+ " wal_bytes_per_sync written. 0 turns it off.");
+
+DEFINE_bool(use_single_deletes, true,
+ "Use single deletes (used in RandomReplaceKeys only).");
+
+DEFINE_double(stddev, 2000.0,
+ "Standard deviation of normal distribution used for picking keys"
+ " (used in RandomReplaceKeys only).");
+
+DEFINE_int32(key_id_range, 100000,
+ "Range of possible value of key id (used in TimeSeries only).");
+
+DEFINE_string(expire_style, "none",
+ "Style to remove expired time entries. Can be one of the options "
+ "below: none (do not expired data), compaction_filter (use a "
+ "compaction filter to remove expired data), delete (seek IDs and "
+ "remove expired data) (used in TimeSeries only).");
+
+DEFINE_uint64(
+ time_range, 100000,
+ "Range of timestamp that store in the database (used in TimeSeries"
+ " only).");
+
+DEFINE_int32(num_deletion_threads, 1,
+ "Number of threads to do deletion (used in TimeSeries and delete "
+ "expire_style only).");
+
+DEFINE_int32(max_successive_merges, 0,
+ "Maximum number of successive merge operations on a key in the "
+ "memtable");
+
+static bool ValidatePrefixSize(const char* flagname, int32_t value) {
+ if (value < 0 || value >= 2000000000) {
+ fprintf(stderr, "Invalid value for --%s: %d. 0<= PrefixSize <=2000000000\n",
+ flagname, value);
+ return false;
+ }
+ return true;
+}
+
+DEFINE_int32(prefix_size, 0,
+ "control the prefix size for HashSkipList and plain table");
+DEFINE_int64(keys_per_prefix, 0,
+ "control average number of keys generated per prefix, 0 means no "
+ "special handling of the prefix, i.e. use the prefix comes with "
+ "the generated random number.");
+DEFINE_bool(total_order_seek, false,
+ "Enable total order seek regardless of index format.");
+DEFINE_bool(prefix_same_as_start, false,
+ "Enforce iterator to return keys with prefix same as seek key.");
+DEFINE_bool(
+ seek_missing_prefix, false,
+ "Iterator seek to keys with non-exist prefixes. Require prefix_size > 8");
+
+DEFINE_int32(memtable_insert_with_hint_prefix_size, 0,
+ "If non-zero, enable "
+ "memtable insert with hint with the given prefix size.");
+DEFINE_bool(enable_io_prio, false,
+ "Lower the background flush/compaction threads' IO priority");
+DEFINE_bool(enable_cpu_prio, false,
+ "Lower the background flush/compaction threads' CPU priority");
+DEFINE_bool(identity_as_first_hash, false,
+ "the first hash function of cuckoo table becomes an identity "
+ "function. This is only valid when key is 8 bytes");
+DEFINE_bool(dump_malloc_stats, true, "Dump malloc stats in LOG ");
+DEFINE_uint64(stats_dump_period_sec,
+ ROCKSDB_NAMESPACE::Options().stats_dump_period_sec,
+ "Gap between printing stats to log in seconds");
+DEFINE_uint64(stats_persist_period_sec,
+ ROCKSDB_NAMESPACE::Options().stats_persist_period_sec,
+ "Gap between persisting stats in seconds");
+DEFINE_bool(persist_stats_to_disk,
+ ROCKSDB_NAMESPACE::Options().persist_stats_to_disk,
+ "whether to persist stats to disk");
+DEFINE_uint64(stats_history_buffer_size,
+ ROCKSDB_NAMESPACE::Options().stats_history_buffer_size,
+ "Max number of stats snapshots to keep in memory");
+DEFINE_bool(avoid_flush_during_recovery,
+ ROCKSDB_NAMESPACE::Options().avoid_flush_during_recovery,
+ "If true, avoids flushing the recovered WAL data where possible.");
+DEFINE_int64(multiread_stride, 0,
+ "Stride length for the keys in a MultiGet batch");
+DEFINE_bool(multiread_batched, false, "Use the new MultiGet API");
+
+DEFINE_string(memtablerep, "skip_list", "");
+DEFINE_int64(hash_bucket_count, 1024 * 1024, "hash bucket count");
+DEFINE_bool(use_plain_table, false,
+ "if use plain table instead of block-based table format");
+DEFINE_bool(use_cuckoo_table, false, "if use cuckoo table format");
+DEFINE_double(cuckoo_hash_ratio, 0.9, "Hash ratio for Cuckoo SST table.");
+DEFINE_bool(use_hash_search, false,
+ "if use kHashSearch instead of kBinarySearch. "
+ "This is valid if only we use BlockTable");
+DEFINE_string(merge_operator, "",
+ "The merge operator to use with the database."
+ "If a new merge operator is specified, be sure to use fresh"
+ " database The possible merge operators are defined in"
+ " utilities/merge_operators.h");
+DEFINE_int32(skip_list_lookahead, 0,
+ "Used with skip_list memtablerep; try linear search first for "
+ "this many steps from the previous position");
+DEFINE_bool(report_file_operations, false,
+ "if report number of file operations");
+DEFINE_bool(report_open_timing, false, "if report open timing");
+DEFINE_int32(readahead_size, 0, "Iterator readahead size");
+
+DEFINE_bool(read_with_latest_user_timestamp, true,
+ "If true, always use the current latest timestamp for read. If "
+ "false, choose a random timestamp from the past.");
+
+#ifndef ROCKSDB_LITE
+DEFINE_string(secondary_cache_uri, "",
+ "Full URI for creating a custom secondary cache object");
+static class std::shared_ptr<ROCKSDB_NAMESPACE::SecondaryCache> secondary_cache;
+#endif // ROCKSDB_LITE
+
+static const bool FLAGS_prefix_size_dummy __attribute__((__unused__)) =
+ RegisterFlagValidator(&FLAGS_prefix_size, &ValidatePrefixSize);
+
+static const bool FLAGS_key_size_dummy __attribute__((__unused__)) =
+ RegisterFlagValidator(&FLAGS_key_size, &ValidateKeySize);
+
+static const bool FLAGS_cache_numshardbits_dummy __attribute__((__unused__)) =
+ RegisterFlagValidator(&FLAGS_cache_numshardbits,
+ &ValidateCacheNumshardbits);
+
+static const bool FLAGS_readwritepercent_dummy __attribute__((__unused__)) =
+ RegisterFlagValidator(&FLAGS_readwritepercent, &ValidateInt32Percent);
+
+DEFINE_int32(disable_seek_compaction, false,
+ "Not used, left here for backwards compatibility");
+
+DEFINE_bool(allow_data_in_errors,
+ ROCKSDB_NAMESPACE::Options().allow_data_in_errors,
+ "If true, allow logging data, e.g. key, value in LOG files.");
+
+static const bool FLAGS_deletepercent_dummy __attribute__((__unused__)) =
+ RegisterFlagValidator(&FLAGS_deletepercent, &ValidateInt32Percent);
+static const bool FLAGS_table_cache_numshardbits_dummy
+ __attribute__((__unused__)) = RegisterFlagValidator(
+ &FLAGS_table_cache_numshardbits, &ValidateTableCacheNumshardbits);
+
+DEFINE_uint32(write_batch_protection_bytes_per_key, 0,
+ "Size of per-key-value checksum in each write batch. Currently "
+ "only value 0 and 8 are supported.");
+
+DEFINE_uint32(
+ memtable_protection_bytes_per_key, 0,
+ "Enable memtable per key-value checksum protection. "
+ "Each entry in memtable will be suffixed by a per key-value checksum. "
+ "This options determines the size of such checksums. "
+ "Supported values: 0, 1, 2, 4, 8.");
+
+DEFINE_bool(build_info, false,
+ "Print the build info via GetRocksBuildInfoAsString");
+
+DEFINE_bool(track_and_verify_wals_in_manifest, false,
+ "If true, enable WAL tracking in the MANIFEST");
+
+namespace ROCKSDB_NAMESPACE {
+namespace {
+static Status CreateMemTableRepFactory(
+ const ConfigOptions& config_options,
+ std::shared_ptr<MemTableRepFactory>* factory) {
+ Status s;
+ if (!strcasecmp(FLAGS_memtablerep.c_str(), SkipListFactory::kNickName())) {
+ factory->reset(new SkipListFactory(FLAGS_skip_list_lookahead));
+#ifndef ROCKSDB_LITE
+ } else if (!strcasecmp(FLAGS_memtablerep.c_str(), "prefix_hash")) {
+ factory->reset(NewHashSkipListRepFactory(FLAGS_hash_bucket_count));
+ } else if (!strcasecmp(FLAGS_memtablerep.c_str(),
+ VectorRepFactory::kNickName())) {
+ factory->reset(new VectorRepFactory());
+ } else if (!strcasecmp(FLAGS_memtablerep.c_str(), "hash_linkedlist")) {
+ factory->reset(NewHashLinkListRepFactory(FLAGS_hash_bucket_count));
+#endif // ROCKSDB_LITE
+ } else {
+ std::unique_ptr<MemTableRepFactory> unique;
+ s = MemTableRepFactory::CreateFromString(config_options, FLAGS_memtablerep,
+ &unique);
+ if (s.ok()) {
+ factory->reset(unique.release());
+ }
+ }
+ return s;
+}
+
+} // namespace
+
+enum DistributionType : unsigned char { kFixed = 0, kUniform, kNormal };
+
+static enum DistributionType FLAGS_value_size_distribution_type_e = kFixed;
+
+static enum DistributionType StringToDistributionType(const char* ctype) {
+ assert(ctype);
+
+ if (!strcasecmp(ctype, "fixed"))
+ return kFixed;
+ else if (!strcasecmp(ctype, "uniform"))
+ return kUniform;
+ else if (!strcasecmp(ctype, "normal"))
+ return kNormal;
+
+ fprintf(stdout, "Cannot parse distribution type '%s'\n", ctype);
+ exit(1);
+}
+
+class BaseDistribution {
+ public:
+ BaseDistribution(unsigned int _min, unsigned int _max)
+ : min_value_size_(_min), max_value_size_(_max) {}
+ virtual ~BaseDistribution() {}
+
+ unsigned int Generate() {
+ auto val = Get();
+ if (NeedTruncate()) {
+ val = std::max(min_value_size_, val);
+ val = std::min(max_value_size_, val);
+ }
+ return val;
+ }
+
+ private:
+ virtual unsigned int Get() = 0;
+ virtual bool NeedTruncate() { return true; }
+ unsigned int min_value_size_;
+ unsigned int max_value_size_;
+};
+
+class FixedDistribution : public BaseDistribution {
+ public:
+ FixedDistribution(unsigned int size)
+ : BaseDistribution(size, size), size_(size) {}
+
+ private:
+ virtual unsigned int Get() override { return size_; }
+ virtual bool NeedTruncate() override { return false; }
+ unsigned int size_;
+};
+
+class NormalDistribution : public BaseDistribution,
+ public std::normal_distribution<double> {
+ public:
+ NormalDistribution(unsigned int _min, unsigned int _max)
+ : BaseDistribution(_min, _max),
+ // 99.7% values within the range [min, max].
+ std::normal_distribution<double>(
+ (double)(_min + _max) / 2.0 /*mean*/,
+ (double)(_max - _min) / 6.0 /*stddev*/),
+ gen_(rd_()) {}
+
+ private:
+ virtual unsigned int Get() override {
+ return static_cast<unsigned int>((*this)(gen_));
+ }
+ std::random_device rd_;
+ std::mt19937 gen_;
+};
+
+class UniformDistribution : public BaseDistribution,
+ public std::uniform_int_distribution<unsigned int> {
+ public:
+ UniformDistribution(unsigned int _min, unsigned int _max)
+ : BaseDistribution(_min, _max),
+ std::uniform_int_distribution<unsigned int>(_min, _max),
+ gen_(rd_()) {}
+
+ private:
+ virtual unsigned int Get() override { return (*this)(gen_); }
+ virtual bool NeedTruncate() override { return false; }
+ std::random_device rd_;
+ std::mt19937 gen_;
+};
+
+// Helper for quickly generating random data.
+class RandomGenerator {
+ private:
+ std::string data_;
+ unsigned int pos_;
+ std::unique_ptr<BaseDistribution> dist_;
+
+ public:
+ RandomGenerator() {
+ auto max_value_size = FLAGS_value_size_max;
+ switch (FLAGS_value_size_distribution_type_e) {
+ case kUniform:
+ dist_.reset(new UniformDistribution(FLAGS_value_size_min,
+ FLAGS_value_size_max));
+ break;
+ case kNormal:
+ dist_.reset(
+ new NormalDistribution(FLAGS_value_size_min, FLAGS_value_size_max));
+ break;
+ case kFixed:
+ default:
+ dist_.reset(new FixedDistribution(value_size));
+ max_value_size = value_size;
+ }
+ // We use a limited amount of data over and over again and ensure
+ // that it is larger than the compression window (32KB), and also
+ // large enough to serve all typical value sizes we want to write.
+ Random rnd(301);
+ std::string piece;
+ while (data_.size() < (unsigned)std::max(1048576, max_value_size)) {
+ // Add a short fragment that is as compressible as specified
+ // by FLAGS_compression_ratio.
+ test::CompressibleString(&rnd, FLAGS_compression_ratio, 100, &piece);
+ data_.append(piece);
+ }
+ pos_ = 0;
+ }
+
+ Slice Generate(unsigned int len) {
+ assert(len <= data_.size());
+ if (pos_ + len > data_.size()) {
+ pos_ = 0;
+ }
+ pos_ += len;
+ return Slice(data_.data() + pos_ - len, len);
+ }
+
+ Slice Generate() {
+ auto len = dist_->Generate();
+ return Generate(len);
+ }
+};
+
+static void AppendWithSpace(std::string* str, Slice msg) {
+ if (msg.empty()) return;
+ if (!str->empty()) {
+ str->push_back(' ');
+ }
+ str->append(msg.data(), msg.size());
+}
+
+struct DBWithColumnFamilies {
+ std::vector<ColumnFamilyHandle*> cfh;
+ DB* db;
+#ifndef ROCKSDB_LITE
+ OptimisticTransactionDB* opt_txn_db;
+#endif // ROCKSDB_LITE
+ std::atomic<size_t> num_created; // Need to be updated after all the
+ // new entries in cfh are set.
+ size_t num_hot; // Number of column families to be queried at each moment.
+ // After each CreateNewCf(), another num_hot number of new
+ // Column families will be created and used to be queried.
+ port::Mutex create_cf_mutex; // Only one thread can execute CreateNewCf()
+ std::vector<int> cfh_idx_to_prob; // ith index holds probability of operating
+ // on cfh[i].
+
+ DBWithColumnFamilies()
+ : db(nullptr)
+#ifndef ROCKSDB_LITE
+ ,
+ opt_txn_db(nullptr)
+#endif // ROCKSDB_LITE
+ {
+ cfh.clear();
+ num_created = 0;
+ num_hot = 0;
+ }
+
+ DBWithColumnFamilies(const DBWithColumnFamilies& other)
+ : cfh(other.cfh),
+ db(other.db),
+#ifndef ROCKSDB_LITE
+ opt_txn_db(other.opt_txn_db),
+#endif // ROCKSDB_LITE
+ num_created(other.num_created.load()),
+ num_hot(other.num_hot),
+ cfh_idx_to_prob(other.cfh_idx_to_prob) {
+ }
+
+ void DeleteDBs() {
+ std::for_each(cfh.begin(), cfh.end(),
+ [](ColumnFamilyHandle* cfhi) { delete cfhi; });
+ cfh.clear();
+#ifndef ROCKSDB_LITE
+ if (opt_txn_db) {
+ delete opt_txn_db;
+ opt_txn_db = nullptr;
+ } else {
+ delete db;
+ db = nullptr;
+ }
+#else
+ delete db;
+ db = nullptr;
+#endif // ROCKSDB_LITE
+ }
+
+ ColumnFamilyHandle* GetCfh(int64_t rand_num) {
+ assert(num_hot > 0);
+ size_t rand_offset = 0;
+ if (!cfh_idx_to_prob.empty()) {
+ assert(cfh_idx_to_prob.size() == num_hot);
+ int sum = 0;
+ while (sum + cfh_idx_to_prob[rand_offset] < rand_num % 100) {
+ sum += cfh_idx_to_prob[rand_offset];
+ ++rand_offset;
+ }
+ assert(rand_offset < cfh_idx_to_prob.size());
+ } else {
+ rand_offset = rand_num % num_hot;
+ }
+ return cfh[num_created.load(std::memory_order_acquire) - num_hot +
+ rand_offset];
+ }
+
+ // stage: assume CF from 0 to stage * num_hot has be created. Need to create
+ // stage * num_hot + 1 to stage * (num_hot + 1).
+ void CreateNewCf(ColumnFamilyOptions options, int64_t stage) {
+ MutexLock l(&create_cf_mutex);
+ if ((stage + 1) * num_hot <= num_created) {
+ // Already created.
+ return;
+ }
+ auto new_num_created = num_created + num_hot;
+ assert(new_num_created <= cfh.size());
+ for (size_t i = num_created; i < new_num_created; i++) {
+ Status s =
+ db->CreateColumnFamily(options, ColumnFamilyName(i), &(cfh[i]));
+ if (!s.ok()) {
+ fprintf(stderr, "create column family error: %s\n",
+ s.ToString().c_str());
+ abort();
+ }
+ }
+ num_created.store(new_num_created, std::memory_order_release);
+ }
+};
+
+// A class that reports stats to CSV file.
+class ReporterAgent {
+ public:
+ ReporterAgent(Env* env, const std::string& fname,
+ uint64_t report_interval_secs)
+ : env_(env),
+ total_ops_done_(0),
+ last_report_(0),
+ report_interval_secs_(report_interval_secs),
+ stop_(false) {
+ auto s = env_->NewWritableFile(fname, &report_file_, EnvOptions());
+ if (s.ok()) {
+ s = report_file_->Append(Header() + "\n");
+ }
+ if (s.ok()) {
+ s = report_file_->Flush();
+ }
+ if (!s.ok()) {
+ fprintf(stderr, "Can't open %s: %s\n", fname.c_str(),
+ s.ToString().c_str());
+ abort();
+ }
+
+ reporting_thread_ = port::Thread([&]() { SleepAndReport(); });
+ }
+
+ ~ReporterAgent() {
+ {
+ std::unique_lock<std::mutex> lk(mutex_);
+ stop_ = true;
+ stop_cv_.notify_all();
+ }
+ reporting_thread_.join();
+ }
+
+ // thread safe
+ void ReportFinishedOps(int64_t num_ops) {
+ total_ops_done_.fetch_add(num_ops);
+ }
+
+ private:
+ std::string Header() const { return "secs_elapsed,interval_qps"; }
+ void SleepAndReport() {
+ auto* clock = env_->GetSystemClock().get();
+ auto time_started = clock->NowMicros();
+ while (true) {
+ {
+ std::unique_lock<std::mutex> lk(mutex_);
+ if (stop_ ||
+ stop_cv_.wait_for(lk, std::chrono::seconds(report_interval_secs_),
+ [&]() { return stop_; })) {
+ // stopping
+ break;
+ }
+ // else -> timeout, which means time for a report!
+ }
+ auto total_ops_done_snapshot = total_ops_done_.load();
+ // round the seconds elapsed
+ auto secs_elapsed =
+ (clock->NowMicros() - time_started + kMicrosInSecond / 2) /
+ kMicrosInSecond;
+ std::string report =
+ std::to_string(secs_elapsed) + "," +
+ std::to_string(total_ops_done_snapshot - last_report_) + "\n";
+ auto s = report_file_->Append(report);
+ if (s.ok()) {
+ s = report_file_->Flush();
+ }
+ if (!s.ok()) {
+ fprintf(stderr,
+ "Can't write to report file (%s), stopping the reporting\n",
+ s.ToString().c_str());
+ break;
+ }
+ last_report_ = total_ops_done_snapshot;
+ }
+ }
+
+ Env* env_;
+ std::unique_ptr<WritableFile> report_file_;
+ std::atomic<int64_t> total_ops_done_;
+ int64_t last_report_;
+ const uint64_t report_interval_secs_;
+ ROCKSDB_NAMESPACE::port::Thread reporting_thread_;
+ std::mutex mutex_;
+ // will notify on stop
+ std::condition_variable stop_cv_;
+ bool stop_;
+};
+
+enum OperationType : unsigned char {
+ kRead = 0,
+ kWrite,
+ kDelete,
+ kSeek,
+ kMerge,
+ kUpdate,
+ kCompress,
+ kUncompress,
+ kCrc,
+ kHash,
+ kOthers
+};
+
+static std::unordered_map<OperationType, std::string, std::hash<unsigned char>>
+ OperationTypeString = {{kRead, "read"}, {kWrite, "write"},
+ {kDelete, "delete"}, {kSeek, "seek"},
+ {kMerge, "merge"}, {kUpdate, "update"},
+ {kCompress, "compress"}, {kCompress, "uncompress"},
+ {kCrc, "crc"}, {kHash, "hash"},
+ {kOthers, "op"}};
+
+class CombinedStats;
+class Stats {
+ private:
+ SystemClock* clock_;
+ int id_;
+ uint64_t start_ = 0;
+ uint64_t sine_interval_;
+ uint64_t finish_;
+ double seconds_;
+ uint64_t done_;
+ uint64_t last_report_done_;
+ uint64_t next_report_;
+ uint64_t bytes_;
+ uint64_t last_op_finish_;
+ uint64_t last_report_finish_;
+ std::unordered_map<OperationType, std::shared_ptr<HistogramImpl>,
+ std::hash<unsigned char>>
+ hist_;
+ std::string message_;
+ bool exclude_from_merge_;
+ ReporterAgent* reporter_agent_; // does not own
+ friend class CombinedStats;
+
+ public:
+ Stats() : clock_(FLAGS_env->GetSystemClock().get()) { Start(-1); }
+
+ void SetReporterAgent(ReporterAgent* reporter_agent) {
+ reporter_agent_ = reporter_agent;
+ }
+
+ void Start(int id) {
+ id_ = id;
+ next_report_ = FLAGS_stats_interval ? FLAGS_stats_interval : 100;
+ last_op_finish_ = start_;
+ hist_.clear();
+ done_ = 0;
+ last_report_done_ = 0;
+ bytes_ = 0;
+ seconds_ = 0;
+ start_ = clock_->NowMicros();
+ sine_interval_ = clock_->NowMicros();
+ finish_ = start_;
+ last_report_finish_ = start_;
+ message_.clear();
+ // When set, stats from this thread won't be merged with others.
+ exclude_from_merge_ = false;
+ }
+
+ void Merge(const Stats& other) {
+ if (other.exclude_from_merge_) return;
+
+ for (auto it = other.hist_.begin(); it != other.hist_.end(); ++it) {
+ auto this_it = hist_.find(it->first);
+ if (this_it != hist_.end()) {
+ this_it->second->Merge(*(other.hist_.at(it->first)));
+ } else {
+ hist_.insert({it->first, it->second});
+ }
+ }
+
+ done_ += other.done_;
+ bytes_ += other.bytes_;
+ seconds_ += other.seconds_;
+ if (other.start_ < start_) start_ = other.start_;
+ if (other.finish_ > finish_) finish_ = other.finish_;
+
+ // Just keep the messages from one thread.
+ if (message_.empty()) message_ = other.message_;
+ }
+
+ void Stop() {
+ finish_ = clock_->NowMicros();
+ seconds_ = (finish_ - start_) * 1e-6;
+ }
+
+ void AddMessage(Slice msg) { AppendWithSpace(&message_, msg); }
+
+ void SetId(int id) { id_ = id; }
+ void SetExcludeFromMerge() { exclude_from_merge_ = true; }
+
+ void PrintThreadStatus() {
+ std::vector<ThreadStatus> thread_list;
+ FLAGS_env->GetThreadList(&thread_list);
+
+ fprintf(stderr, "\n%18s %10s %12s %20s %13s %45s %12s %s\n", "ThreadID",
+ "ThreadType", "cfName", "Operation", "ElapsedTime", "Stage",
+ "State", "OperationProperties");
+
+ int64_t current_time = 0;
+ clock_->GetCurrentTime(&current_time).PermitUncheckedError();
+ for (auto ts : thread_list) {
+ fprintf(stderr, "%18" PRIu64 " %10s %12s %20s %13s %45s %12s",
+ ts.thread_id,
+ ThreadStatus::GetThreadTypeName(ts.thread_type).c_str(),
+ ts.cf_name.c_str(),
+ ThreadStatus::GetOperationName(ts.operation_type).c_str(),
+ ThreadStatus::MicrosToString(ts.op_elapsed_micros).c_str(),
+ ThreadStatus::GetOperationStageName(ts.operation_stage).c_str(),
+ ThreadStatus::GetStateName(ts.state_type).c_str());
+
+ auto op_properties = ThreadStatus::InterpretOperationProperties(
+ ts.operation_type, ts.op_properties);
+ for (const auto& op_prop : op_properties) {
+ fprintf(stderr, " %s %" PRIu64 " |", op_prop.first.c_str(),
+ op_prop.second);
+ }
+ fprintf(stderr, "\n");
+ }
+ }
+
+ void ResetSineInterval() { sine_interval_ = clock_->NowMicros(); }
+
+ uint64_t GetSineInterval() { return sine_interval_; }
+
+ uint64_t GetStart() { return start_; }
+
+ void ResetLastOpTime() {
+ // Set to now to avoid latency from calls to SleepForMicroseconds.
+ last_op_finish_ = clock_->NowMicros();
+ }
+
+ void FinishedOps(DBWithColumnFamilies* db_with_cfh, DB* db, int64_t num_ops,
+ enum OperationType op_type = kOthers) {
+ if (reporter_agent_) {
+ reporter_agent_->ReportFinishedOps(num_ops);
+ }
+ if (FLAGS_histogram) {
+ uint64_t now = clock_->NowMicros();
+ uint64_t micros = now - last_op_finish_;
+
+ if (hist_.find(op_type) == hist_.end()) {
+ auto hist_temp = std::make_shared<HistogramImpl>();
+ hist_.insert({op_type, std::move(hist_temp)});
+ }
+ hist_[op_type]->Add(micros);
+
+ if (micros >= FLAGS_slow_usecs && !FLAGS_stats_interval) {
+ fprintf(stderr, "long op: %" PRIu64 " micros%30s\r", micros, "");
+ fflush(stderr);
+ }
+ last_op_finish_ = now;
+ }
+
+ done_ += num_ops;
+ if (done_ >= next_report_ && FLAGS_progress_reports) {
+ if (!FLAGS_stats_interval) {
+ if (next_report_ < 1000)
+ next_report_ += 100;
+ else if (next_report_ < 5000)
+ next_report_ += 500;
+ else if (next_report_ < 10000)
+ next_report_ += 1000;
+ else if (next_report_ < 50000)
+ next_report_ += 5000;
+ else if (next_report_ < 100000)
+ next_report_ += 10000;
+ else if (next_report_ < 500000)
+ next_report_ += 50000;
+ else
+ next_report_ += 100000;
+ fprintf(stderr, "... finished %" PRIu64 " ops%30s\r", done_, "");
+ } else {
+ uint64_t now = clock_->NowMicros();
+ int64_t usecs_since_last = now - last_report_finish_;
+
+ // Determine whether to print status where interval is either
+ // each N operations or each N seconds.
+
+ if (FLAGS_stats_interval_seconds &&
+ usecs_since_last < (FLAGS_stats_interval_seconds * 1000000)) {
+ // Don't check again for this many operations.
+ next_report_ += FLAGS_stats_interval;
+
+ } else {
+ fprintf(stderr,
+ "%s ... thread %d: (%" PRIu64 ",%" PRIu64
+ ") ops and "
+ "(%.1f,%.1f) ops/second in (%.6f,%.6f) seconds\n",
+ clock_->TimeToString(now / 1000000).c_str(), id_,
+ done_ - last_report_done_, done_,
+ (done_ - last_report_done_) / (usecs_since_last / 1000000.0),
+ done_ / ((now - start_) / 1000000.0),
+ (now - last_report_finish_) / 1000000.0,
+ (now - start_) / 1000000.0);
+
+ if (id_ == 0 && FLAGS_stats_per_interval) {
+ std::string stats;
+
+ if (db_with_cfh && db_with_cfh->num_created.load()) {
+ for (size_t i = 0; i < db_with_cfh->num_created.load(); ++i) {
+ if (db->GetProperty(db_with_cfh->cfh[i], "rocksdb.cfstats",
+ &stats))
+ fprintf(stderr, "%s\n", stats.c_str());
+ if (FLAGS_show_table_properties) {
+ for (int level = 0; level < FLAGS_num_levels; ++level) {
+ if (db->GetProperty(
+ db_with_cfh->cfh[i],
+ "rocksdb.aggregated-table-properties-at-level" +
+ std::to_string(level),
+ &stats)) {
+ if (stats.find("# entries=0") == std::string::npos) {
+ fprintf(stderr, "Level[%d]: %s\n", level,
+ stats.c_str());
+ }
+ }
+ }
+ }
+ }
+ } else if (db) {
+ if (db->GetProperty("rocksdb.stats", &stats)) {
+ fprintf(stderr, "%s", stats.c_str());
+ }
+ if (db->GetProperty("rocksdb.num-running-compactions", &stats)) {
+ fprintf(stderr, "num-running-compactions: %s\n", stats.c_str());
+ }
+ if (db->GetProperty("rocksdb.num-running-flushes", &stats)) {
+ fprintf(stderr, "num-running-flushes: %s\n\n", stats.c_str());
+ }
+ if (FLAGS_show_table_properties) {
+ for (int level = 0; level < FLAGS_num_levels; ++level) {
+ if (db->GetProperty(
+ "rocksdb.aggregated-table-properties-at-level" +
+ std::to_string(level),
+ &stats)) {
+ if (stats.find("# entries=0") == std::string::npos) {
+ fprintf(stderr, "Level[%d]: %s\n", level, stats.c_str());
+ }
+ }
+ }
+ }
+ }
+ }
+
+ next_report_ += FLAGS_stats_interval;
+ last_report_finish_ = now;
+ last_report_done_ = done_;
+ }
+ }
+ if (id_ == 0 && FLAGS_thread_status_per_interval) {
+ PrintThreadStatus();
+ }
+ fflush(stderr);
+ }
+ }
+
+ void AddBytes(int64_t n) { bytes_ += n; }
+
+ void Report(const Slice& name) {
+ // Pretend at least one op was done in case we are running a benchmark
+ // that does not call FinishedOps().
+ if (done_ < 1) done_ = 1;
+
+ std::string extra;
+ double elapsed = (finish_ - start_) * 1e-6;
+ if (bytes_ > 0) {
+ // Rate is computed on actual elapsed time, not the sum of per-thread
+ // elapsed times.
+ char rate[100];
+ snprintf(rate, sizeof(rate), "%6.1f MB/s",
+ (bytes_ / 1048576.0) / elapsed);
+ extra = rate;
+ }
+ AppendWithSpace(&extra, message_);
+ double throughput = (double)done_ / elapsed;
+
+ fprintf(stdout,
+ "%-12s : %11.3f micros/op %ld ops/sec %.3f seconds %" PRIu64
+ " operations;%s%s\n",
+ name.ToString().c_str(), seconds_ * 1e6 / done_, (long)throughput,
+ elapsed, done_, (extra.empty() ? "" : " "), extra.c_str());
+ if (FLAGS_histogram) {
+ for (auto it = hist_.begin(); it != hist_.end(); ++it) {
+ fprintf(stdout, "Microseconds per %s:\n%s\n",
+ OperationTypeString[it->first].c_str(),
+ it->second->ToString().c_str());
+ }
+ }
+ if (FLAGS_report_file_operations) {
+ auto* counted_fs =
+ FLAGS_env->GetFileSystem()->CheckedCast<CountedFileSystem>();
+ assert(counted_fs);
+ fprintf(stdout, "%s", counted_fs->PrintCounters().c_str());
+ counted_fs->ResetCounters();
+ }
+ fflush(stdout);
+ }
+};
+
+class CombinedStats {
+ public:
+ void AddStats(const Stats& stat) {
+ uint64_t total_ops = stat.done_;
+ uint64_t total_bytes_ = stat.bytes_;
+ double elapsed;
+
+ if (total_ops < 1) {
+ total_ops = 1;
+ }
+
+ elapsed = (stat.finish_ - stat.start_) * 1e-6;
+ throughput_ops_.emplace_back(total_ops / elapsed);
+
+ if (total_bytes_ > 0) {
+ double mbs = (total_bytes_ / 1048576.0);
+ throughput_mbs_.emplace_back(mbs / elapsed);
+ }
+ }
+
+ void Report(const std::string& bench_name) {
+ if (throughput_ops_.size() < 2) {
+ // skip if there are not enough samples
+ return;
+ }
+
+ const char* name = bench_name.c_str();
+ int num_runs = static_cast<int>(throughput_ops_.size());
+
+ if (throughput_mbs_.size() == throughput_ops_.size()) {
+ fprintf(stdout,
+ "%s [AVG %d runs] : %d (\xC2\xB1 %d) ops/sec; %6.1f (\xC2\xB1 "
+ "%.1f) MB/sec\n",
+ name, num_runs, static_cast<int>(CalcAvg(throughput_ops_)),
+ static_cast<int>(CalcConfidence95(throughput_ops_)),
+ CalcAvg(throughput_mbs_), CalcConfidence95(throughput_mbs_));
+ } else {
+ fprintf(stdout, "%s [AVG %d runs] : %d (\xC2\xB1 %d) ops/sec\n", name,
+ num_runs, static_cast<int>(CalcAvg(throughput_ops_)),
+ static_cast<int>(CalcConfidence95(throughput_ops_)));
+ }
+ }
+
+ void ReportWithConfidenceIntervals(const std::string& bench_name) {
+ if (throughput_ops_.size() < 2) {
+ // skip if there are not enough samples
+ return;
+ }
+
+ const char* name = bench_name.c_str();
+ int num_runs = static_cast<int>(throughput_ops_.size());
+
+ int ops_avg = static_cast<int>(CalcAvg(throughput_ops_));
+ int ops_confidence_95 = static_cast<int>(CalcConfidence95(throughput_ops_));
+
+ if (throughput_mbs_.size() == throughput_ops_.size()) {
+ double mbs_avg = CalcAvg(throughput_mbs_);
+ double mbs_confidence_95 = CalcConfidence95(throughput_mbs_);
+ fprintf(stdout,
+ "%s [CI95 %d runs] : (%d, %d) ops/sec; (%.1f, %.1f) MB/sec\n",
+ name, num_runs, ops_avg - ops_confidence_95,
+ ops_avg + ops_confidence_95, mbs_avg - mbs_confidence_95,
+ mbs_avg + mbs_confidence_95);
+ } else {
+ fprintf(stdout, "%s [CI95 %d runs] : (%d, %d) ops/sec\n", name, num_runs,
+ ops_avg - ops_confidence_95, ops_avg + ops_confidence_95);
+ }
+ }
+
+ void ReportFinal(const std::string& bench_name) {
+ if (throughput_ops_.size() < 2) {
+ // skip if there are not enough samples
+ return;
+ }
+
+ const char* name = bench_name.c_str();
+ int num_runs = static_cast<int>(throughput_ops_.size());
+
+ if (throughput_mbs_.size() == throughput_ops_.size()) {
+ // \xC2\xB1 is +/- character in UTF-8
+ fprintf(stdout,
+ "%s [AVG %d runs] : %d (\xC2\xB1 %d) ops/sec; %6.1f (\xC2\xB1 "
+ "%.1f) MB/sec\n"
+ "%s [MEDIAN %d runs] : %d ops/sec; %6.1f MB/sec\n",
+ name, num_runs, static_cast<int>(CalcAvg(throughput_ops_)),
+ static_cast<int>(CalcConfidence95(throughput_ops_)),
+ CalcAvg(throughput_mbs_), CalcConfidence95(throughput_mbs_), name,
+ num_runs, static_cast<int>(CalcMedian(throughput_ops_)),
+ CalcMedian(throughput_mbs_));
+ } else {
+ fprintf(stdout,
+ "%s [AVG %d runs] : %d (\xC2\xB1 %d) ops/sec\n"
+ "%s [MEDIAN %d runs] : %d ops/sec\n",
+ name, num_runs, static_cast<int>(CalcAvg(throughput_ops_)),
+ static_cast<int>(CalcConfidence95(throughput_ops_)), name,
+ num_runs, static_cast<int>(CalcMedian(throughput_ops_)));
+ }
+ }
+
+ private:
+ double CalcAvg(std::vector<double>& data) {
+ double avg = 0;
+ for (double x : data) {
+ avg += x;
+ }
+ avg = avg / data.size();
+ return avg;
+ }
+
+ // Calculates 95% CI assuming a normal distribution of samples.
+ // Samples are not from a normal distribution, but it still
+ // provides useful approximation.
+ double CalcConfidence95(std::vector<double>& data) {
+ assert(data.size() > 1);
+ double avg = CalcAvg(data);
+ double std_error = CalcStdDev(data, avg) / std::sqrt(data.size());
+
+ // Z score for the 97.5 percentile
+ // see https://en.wikipedia.org/wiki/1.96
+ return 1.959964 * std_error;
+ }
+
+ double CalcMedian(std::vector<double>& data) {
+ assert(data.size() > 0);
+ std::sort(data.begin(), data.end());
+
+ size_t mid = data.size() / 2;
+ if (data.size() % 2 == 1) {
+ // Odd number of entries
+ return data[mid];
+ } else {
+ // Even number of entries
+ return (data[mid] + data[mid - 1]) / 2;
+ }
+ }
+
+ double CalcStdDev(std::vector<double>& data, double average) {
+ assert(data.size() > 1);
+ double squared_sum = 0.0;
+ for (double x : data) {
+ squared_sum += std::pow(x - average, 2);
+ }
+
+ // using samples count - 1 following Bessel's correction
+ // see https://en.wikipedia.org/wiki/Bessel%27s_correction
+ return std::sqrt(squared_sum / (data.size() - 1));
+ }
+
+ std::vector<double> throughput_ops_;
+ std::vector<double> throughput_mbs_;
+};
+
+class TimestampEmulator {
+ private:
+ std::atomic<uint64_t> timestamp_;
+
+ public:
+ TimestampEmulator() : timestamp_(0) {}
+ uint64_t Get() const { return timestamp_.load(); }
+ void Inc() { timestamp_++; }
+ Slice Allocate(char* scratch) {
+ // TODO: support larger timestamp sizes
+ assert(FLAGS_user_timestamp_size == 8);
+ assert(scratch);
+ uint64_t ts = timestamp_.fetch_add(1);
+ EncodeFixed64(scratch, ts);
+ return Slice(scratch, FLAGS_user_timestamp_size);
+ }
+ Slice GetTimestampForRead(Random64& rand, char* scratch) {
+ assert(FLAGS_user_timestamp_size == 8);
+ assert(scratch);
+ if (FLAGS_read_with_latest_user_timestamp) {
+ return Allocate(scratch);
+ }
+ // Choose a random timestamp from the past.
+ uint64_t ts = rand.Next() % Get();
+ EncodeFixed64(scratch, ts);
+ return Slice(scratch, FLAGS_user_timestamp_size);
+ }
+};
+
+// State shared by all concurrent executions of the same benchmark.
+struct SharedState {
+ port::Mutex mu;
+ port::CondVar cv;
+ int total;
+ int perf_level;
+ std::shared_ptr<RateLimiter> write_rate_limiter;
+ std::shared_ptr<RateLimiter> read_rate_limiter;
+
+ // Each thread goes through the following states:
+ // (1) initializing
+ // (2) waiting for others to be initialized
+ // (3) running
+ // (4) done
+
+ long num_initialized;
+ long num_done;
+ bool start;
+
+ SharedState() : cv(&mu), perf_level(FLAGS_perf_level) {}
+};
+
+// Per-thread state for concurrent executions of the same benchmark.
+struct ThreadState {
+ int tid; // 0..n-1 when running in n threads
+ Random64 rand; // Has different seeds for different threads
+ Stats stats;
+ SharedState* shared;
+
+ explicit ThreadState(int index, int my_seed)
+ : tid(index), rand(seed_base + my_seed) {}
+};
+
+class Duration {
+ public:
+ Duration(uint64_t max_seconds, int64_t max_ops, int64_t ops_per_stage = 0) {
+ max_seconds_ = max_seconds;
+ max_ops_ = max_ops;
+ ops_per_stage_ = (ops_per_stage > 0) ? ops_per_stage : max_ops;
+ ops_ = 0;
+ start_at_ = FLAGS_env->NowMicros();
+ }
+
+ int64_t GetStage() { return std::min(ops_, max_ops_ - 1) / ops_per_stage_; }
+
+ bool Done(int64_t increment) {
+ if (increment <= 0) increment = 1; // avoid Done(0) and infinite loops
+ ops_ += increment;
+
+ if (max_seconds_) {
+ // Recheck every appx 1000 ops (exact iff increment is factor of 1000)
+ auto granularity = FLAGS_ops_between_duration_checks;
+ if ((ops_ / granularity) != ((ops_ - increment) / granularity)) {
+ uint64_t now = FLAGS_env->NowMicros();
+ return ((now - start_at_) / 1000000) >= max_seconds_;
+ } else {
+ return false;
+ }
+ } else {
+ return ops_ > max_ops_;
+ }
+ }
+
+ private:
+ uint64_t max_seconds_;
+ int64_t max_ops_;
+ int64_t ops_per_stage_;
+ int64_t ops_;
+ uint64_t start_at_;
+};
+
+class Benchmark {
+ private:
+ std::shared_ptr<Cache> cache_;
+ std::shared_ptr<Cache> compressed_cache_;
+ std::shared_ptr<const SliceTransform> prefix_extractor_;
+ DBWithColumnFamilies db_;
+ std::vector<DBWithColumnFamilies> multi_dbs_;
+ int64_t num_;
+ int key_size_;
+ int user_timestamp_size_;
+ int prefix_size_;
+ int total_thread_count_;
+ int64_t keys_per_prefix_;
+ int64_t entries_per_batch_;
+ int64_t writes_before_delete_range_;
+ int64_t writes_per_range_tombstone_;
+ int64_t range_tombstone_width_;
+ int64_t max_num_range_tombstones_;
+ ReadOptions read_options_;
+ WriteOptions write_options_;
+ Options open_options_; // keep options around to properly destroy db later
+#ifndef ROCKSDB_LITE
+ TraceOptions trace_options_;
+ TraceOptions block_cache_trace_options_;
+#endif
+ int64_t reads_;
+ int64_t deletes_;
+ double read_random_exp_range_;
+ int64_t writes_;
+ int64_t readwrites_;
+ int64_t merge_keys_;
+ bool report_file_operations_;
+ bool use_blob_db_; // Stacked BlobDB
+ bool read_operands_; // read via GetMergeOperands()
+ std::vector<std::string> keys_;
+
+ class ErrorHandlerListener : public EventListener {
+ public:
+#ifndef ROCKSDB_LITE
+ ErrorHandlerListener()
+ : mutex_(),
+ cv_(&mutex_),
+ no_auto_recovery_(false),
+ recovery_complete_(false) {}
+
+ ~ErrorHandlerListener() override {}
+
+ const char* Name() const override { return kClassName(); }
+ static const char* kClassName() { return "ErrorHandlerListener"; }
+
+ void OnErrorRecoveryBegin(BackgroundErrorReason /*reason*/,
+ Status /*bg_error*/,
+ bool* auto_recovery) override {
+ if (*auto_recovery && no_auto_recovery_) {
+ *auto_recovery = false;
+ }
+ }
+
+ void OnErrorRecoveryCompleted(Status /*old_bg_error*/) override {
+ InstrumentedMutexLock l(&mutex_);
+ recovery_complete_ = true;
+ cv_.SignalAll();
+ }
+
+ bool WaitForRecovery(uint64_t abs_time_us) {
+ InstrumentedMutexLock l(&mutex_);
+ if (!recovery_complete_) {
+ cv_.TimedWait(abs_time_us);
+ }
+ if (recovery_complete_) {
+ recovery_complete_ = false;
+ return true;
+ }
+ return false;
+ }
+
+ void EnableAutoRecovery(bool enable = true) { no_auto_recovery_ = !enable; }
+
+ private:
+ InstrumentedMutex mutex_;
+ InstrumentedCondVar cv_;
+ bool no_auto_recovery_;
+ bool recovery_complete_;
+#else // ROCKSDB_LITE
+ bool WaitForRecovery(uint64_t /*abs_time_us*/) { return true; }
+ void EnableAutoRecovery(bool /*enable*/) {}
+#endif // ROCKSDB_LITE
+ };
+
+ std::shared_ptr<ErrorHandlerListener> listener_;
+
+ std::unique_ptr<TimestampEmulator> mock_app_clock_;
+
+ bool SanityCheck() {
+ if (FLAGS_compression_ratio > 1) {
+ fprintf(stderr, "compression_ratio should be between 0 and 1\n");
+ return false;
+ }
+ return true;
+ }
+
+ inline bool CompressSlice(const CompressionInfo& compression_info,
+ const Slice& input, std::string* compressed) {
+ constexpr uint32_t compress_format_version = 2;
+
+ return CompressData(input, compression_info, compress_format_version,
+ compressed);
+ }
+
+ void PrintHeader(const Options& options) {
+ PrintEnvironment();
+ fprintf(stdout,
+ "Keys: %d bytes each (+ %d bytes user-defined timestamp)\n",
+ FLAGS_key_size, FLAGS_user_timestamp_size);
+ auto avg_value_size = FLAGS_value_size;
+ if (FLAGS_value_size_distribution_type_e == kFixed) {
+ fprintf(stdout,
+ "Values: %d bytes each (%d bytes after compression)\n",
+ avg_value_size,
+ static_cast<int>(avg_value_size * FLAGS_compression_ratio + 0.5));
+ } else {
+ avg_value_size = (FLAGS_value_size_min + FLAGS_value_size_max) / 2;
+ fprintf(stdout,
+ "Values: %d avg bytes each (%d bytes after compression)\n",
+ avg_value_size,
+ static_cast<int>(avg_value_size * FLAGS_compression_ratio + 0.5));
+ fprintf(stdout, "Values Distribution: %s (min: %d, max: %d)\n",
+ FLAGS_value_size_distribution_type.c_str(), FLAGS_value_size_min,
+ FLAGS_value_size_max);
+ }
+ fprintf(stdout, "Entries: %" PRIu64 "\n", num_);
+ fprintf(stdout, "Prefix: %d bytes\n", FLAGS_prefix_size);
+ fprintf(stdout, "Keys per prefix: %" PRIu64 "\n", keys_per_prefix_);
+ fprintf(stdout, "RawSize: %.1f MB (estimated)\n",
+ ((static_cast<int64_t>(FLAGS_key_size + avg_value_size) * num_) /
+ 1048576.0));
+ fprintf(
+ stdout, "FileSize: %.1f MB (estimated)\n",
+ (((FLAGS_key_size + avg_value_size * FLAGS_compression_ratio) * num_) /
+ 1048576.0));
+ fprintf(stdout, "Write rate: %" PRIu64 " bytes/second\n",
+ FLAGS_benchmark_write_rate_limit);
+ fprintf(stdout, "Read rate: %" PRIu64 " ops/second\n",
+ FLAGS_benchmark_read_rate_limit);
+ if (FLAGS_enable_numa) {
+ fprintf(stderr, "Running in NUMA enabled mode.\n");
+#ifndef NUMA
+ fprintf(stderr, "NUMA is not defined in the system.\n");
+ exit(1);
+#else
+ if (numa_available() == -1) {
+ fprintf(stderr, "NUMA is not supported by the system.\n");
+ exit(1);
+ }
+#endif
+ }
+
+ auto compression = CompressionTypeToString(FLAGS_compression_type_e);
+ fprintf(stdout, "Compression: %s\n", compression.c_str());
+ fprintf(stdout, "Compression sampling rate: %" PRId64 "\n",
+ FLAGS_sample_for_compression);
+ if (options.memtable_factory != nullptr) {
+ fprintf(stdout, "Memtablerep: %s\n",
+ options.memtable_factory->GetId().c_str());
+ }
+ fprintf(stdout, "Perf Level: %d\n", FLAGS_perf_level);
+
+ PrintWarnings(compression.c_str());
+ fprintf(stdout, "------------------------------------------------\n");
+ }
+
+ void PrintWarnings(const char* compression) {
+#if defined(__GNUC__) && !defined(__OPTIMIZE__)
+ fprintf(
+ stdout,
+ "WARNING: Optimization is disabled: benchmarks unnecessarily slow\n");
+#endif
+#ifndef NDEBUG
+ fprintf(stdout,
+ "WARNING: Assertions are enabled; benchmarks unnecessarily slow\n");
+#endif
+ if (FLAGS_compression_type_e != ROCKSDB_NAMESPACE::kNoCompression) {
+ // The test string should not be too small.
+ const int len = FLAGS_block_size;
+ std::string input_str(len, 'y');
+ std::string compressed;
+ CompressionOptions opts;
+ CompressionContext context(FLAGS_compression_type_e);
+ CompressionInfo info(opts, context, CompressionDict::GetEmptyDict(),
+ FLAGS_compression_type_e,
+ FLAGS_sample_for_compression);
+ bool result = CompressSlice(info, Slice(input_str), &compressed);
+
+ if (!result) {
+ fprintf(stdout, "WARNING: %s compression is not enabled\n",
+ compression);
+ } else if (compressed.size() >= input_str.size()) {
+ fprintf(stdout, "WARNING: %s compression is not effective\n",
+ compression);
+ }
+ }
+ }
+
+// Current the following isn't equivalent to OS_LINUX.
+#if defined(__linux)
+ static Slice TrimSpace(Slice s) {
+ unsigned int start = 0;
+ while (start < s.size() && isspace(s[start])) {
+ start++;
+ }
+ unsigned int limit = static_cast<unsigned int>(s.size());
+ while (limit > start && isspace(s[limit - 1])) {
+ limit--;
+ }
+ return Slice(s.data() + start, limit - start);
+ }
+#endif
+
+ void PrintEnvironment() {
+ fprintf(stderr, "RocksDB: version %s\n",
+ GetRocksVersionAsString(true).c_str());
+
+#if defined(__linux) || defined(__APPLE__) || defined(__FreeBSD__)
+ time_t now = time(nullptr);
+ char buf[52];
+ // Lint complains about ctime() usage, so replace it with ctime_r(). The
+ // requirement is to provide a buffer which is at least 26 bytes.
+ fprintf(stderr, "Date: %s",
+ ctime_r(&now, buf)); // ctime_r() adds newline
+
+#if defined(__linux)
+ FILE* cpuinfo = fopen("/proc/cpuinfo", "r");
+ if (cpuinfo != nullptr) {
+ char line[1000];
+ int num_cpus = 0;
+ std::string cpu_type;
+ std::string cache_size;
+ while (fgets(line, sizeof(line), cpuinfo) != nullptr) {
+ const char* sep = strchr(line, ':');
+ if (sep == nullptr) {
+ continue;
+ }
+ Slice key = TrimSpace(Slice(line, sep - 1 - line));
+ Slice val = TrimSpace(Slice(sep + 1));
+ if (key == "model name") {
+ ++num_cpus;
+ cpu_type = val.ToString();
+ } else if (key == "cache size") {
+ cache_size = val.ToString();
+ }
+ }
+ fclose(cpuinfo);
+ fprintf(stderr, "CPU: %d * %s\n", num_cpus, cpu_type.c_str());
+ fprintf(stderr, "CPUCache: %s\n", cache_size.c_str());
+ }
+#elif defined(__APPLE__)
+ struct host_basic_info h;
+ size_t hlen = HOST_BASIC_INFO_COUNT;
+ if (host_info(mach_host_self(), HOST_BASIC_INFO, (host_info_t)&h,
+ (uint32_t*)&hlen) == KERN_SUCCESS) {
+ std::string cpu_type;
+ std::string cache_size;
+ size_t hcache_size;
+ hlen = sizeof(hcache_size);
+ if (sysctlbyname("hw.cachelinesize", &hcache_size, &hlen, NULL, 0) == 0) {
+ cache_size = std::to_string(hcache_size);
+ }
+ switch (h.cpu_type) {
+ case CPU_TYPE_X86_64:
+ cpu_type = "x86_64";
+ break;
+ case CPU_TYPE_ARM64:
+ cpu_type = "arm64";
+ break;
+ default:
+ break;
+ }
+ fprintf(stderr, "CPU: %d * %s\n", h.max_cpus, cpu_type.c_str());
+ fprintf(stderr, "CPUCache: %s\n", cache_size.c_str());
+ }
+#elif defined(__FreeBSD__)
+ int ncpus;
+ size_t len = sizeof(ncpus);
+ int mib[2] = {CTL_HW, HW_NCPU};
+ if (sysctl(mib, 2, &ncpus, &len, nullptr, 0) == 0) {
+ char cpu_type[16];
+ len = sizeof(cpu_type) - 1;
+ mib[1] = HW_MACHINE;
+ if (sysctl(mib, 2, cpu_type, &len, nullptr, 0) == 0) cpu_type[len] = 0;
+
+ fprintf(stderr, "CPU: %d * %s\n", ncpus, cpu_type);
+ // no programmatic way to get the cache line size except on PPC
+ }
+#endif
+#endif
+ }
+
+ static bool KeyExpired(const TimestampEmulator* timestamp_emulator,
+ const Slice& key) {
+ const char* pos = key.data();
+ pos += 8;
+ uint64_t timestamp = 0;
+ if (port::kLittleEndian) {
+ int bytes_to_fill = 8;
+ for (int i = 0; i < bytes_to_fill; ++i) {
+ timestamp |= (static_cast<uint64_t>(static_cast<unsigned char>(pos[i]))
+ << ((bytes_to_fill - i - 1) << 3));
+ }
+ } else {
+ memcpy(&timestamp, pos, sizeof(timestamp));
+ }
+ return timestamp_emulator->Get() - timestamp > FLAGS_time_range;
+ }
+
+ class ExpiredTimeFilter : public CompactionFilter {
+ public:
+ explicit ExpiredTimeFilter(
+ const std::shared_ptr<TimestampEmulator>& timestamp_emulator)
+ : timestamp_emulator_(timestamp_emulator) {}
+ bool Filter(int /*level*/, const Slice& key,
+ const Slice& /*existing_value*/, std::string* /*new_value*/,
+ bool* /*value_changed*/) const override {
+ return KeyExpired(timestamp_emulator_.get(), key);
+ }
+ const char* Name() const override { return "ExpiredTimeFilter"; }
+
+ private:
+ std::shared_ptr<TimestampEmulator> timestamp_emulator_;
+ };
+
+ class KeepFilter : public CompactionFilter {
+ public:
+ bool Filter(int /*level*/, const Slice& /*key*/, const Slice& /*value*/,
+ std::string* /*new_value*/,
+ bool* /*value_changed*/) const override {
+ return false;
+ }
+
+ const char* Name() const override { return "KeepFilter"; }
+ };
+
+ static std::shared_ptr<MemoryAllocator> GetCacheAllocator() {
+ std::shared_ptr<MemoryAllocator> allocator;
+
+ if (FLAGS_use_cache_jemalloc_no_dump_allocator) {
+ JemallocAllocatorOptions jemalloc_options;
+ if (!NewJemallocNodumpAllocator(jemalloc_options, &allocator).ok()) {
+ fprintf(stderr, "JemallocNodumpAllocator not supported.\n");
+ exit(1);
+ }
+ } else if (FLAGS_use_cache_memkind_kmem_allocator) {
+#ifdef MEMKIND
+ allocator = std::make_shared<MemkindKmemAllocator>();
+#else
+ fprintf(stderr, "Memkind library is not linked with the binary.\n");
+ exit(1);
+#endif
+ }
+
+ return allocator;
+ }
+
+ static std::shared_ptr<Cache> NewCache(int64_t capacity) {
+ if (capacity <= 0) {
+ return nullptr;
+ }
+ if (FLAGS_cache_type == "clock_cache") {
+ fprintf(stderr, "Old clock cache implementation has been removed.\n");
+ exit(1);
+ } else if (FLAGS_cache_type == "hyper_clock_cache") {
+ return HyperClockCacheOptions(static_cast<size_t>(capacity),
+ FLAGS_block_size /*estimated_entry_charge*/,
+ FLAGS_cache_numshardbits)
+ .MakeSharedCache();
+ } else if (FLAGS_cache_type == "lru_cache") {
+ LRUCacheOptions opts(
+ static_cast<size_t>(capacity), FLAGS_cache_numshardbits,
+ false /*strict_capacity_limit*/, FLAGS_cache_high_pri_pool_ratio,
+ GetCacheAllocator(), kDefaultToAdaptiveMutex,
+ kDefaultCacheMetadataChargePolicy, FLAGS_cache_low_pri_pool_ratio);
+
+#ifndef ROCKSDB_LITE
+ if (!FLAGS_secondary_cache_uri.empty()) {
+ Status s = SecondaryCache::CreateFromString(
+ ConfigOptions(), FLAGS_secondary_cache_uri, &secondary_cache);
+ if (secondary_cache == nullptr) {
+ fprintf(
+ stderr,
+ "No secondary cache registered matching string: %s status=%s\n",
+ FLAGS_secondary_cache_uri.c_str(), s.ToString().c_str());
+ exit(1);
+ }
+ opts.secondary_cache = secondary_cache;
+ }
+#endif // ROCKSDB_LITE
+
+ if (FLAGS_use_compressed_secondary_cache) {
+ CompressedSecondaryCacheOptions secondary_cache_opts;
+ secondary_cache_opts.capacity = FLAGS_compressed_secondary_cache_size;
+ secondary_cache_opts.num_shard_bits =
+ FLAGS_compressed_secondary_cache_numshardbits;
+ secondary_cache_opts.high_pri_pool_ratio =
+ FLAGS_compressed_secondary_cache_high_pri_pool_ratio;
+ secondary_cache_opts.low_pri_pool_ratio =
+ FLAGS_compressed_secondary_cache_low_pri_pool_ratio;
+ secondary_cache_opts.compression_type =
+ FLAGS_compressed_secondary_cache_compression_type_e;
+ secondary_cache_opts.compress_format_version =
+ FLAGS_compressed_secondary_cache_compress_format_version;
+ opts.secondary_cache =
+ NewCompressedSecondaryCache(secondary_cache_opts);
+ }
+
+ return NewLRUCache(opts);
+ } else {
+ fprintf(stderr, "Cache type not supported.");
+ exit(1);
+ }
+ }
+
+ public:
+ Benchmark()
+ : cache_(NewCache(FLAGS_cache_size)),
+ compressed_cache_(NewCache(FLAGS_compressed_cache_size)),
+ prefix_extractor_(FLAGS_prefix_size != 0
+ ? NewFixedPrefixTransform(FLAGS_prefix_size)
+ : nullptr),
+ num_(FLAGS_num),
+ key_size_(FLAGS_key_size),
+ user_timestamp_size_(FLAGS_user_timestamp_size),
+ prefix_size_(FLAGS_prefix_size),
+ total_thread_count_(0),
+ keys_per_prefix_(FLAGS_keys_per_prefix),
+ entries_per_batch_(1),
+ reads_(FLAGS_reads < 0 ? FLAGS_num : FLAGS_reads),
+ read_random_exp_range_(0.0),
+ writes_(FLAGS_writes < 0 ? FLAGS_num : FLAGS_writes),
+ readwrites_(
+ (FLAGS_writes < 0 && FLAGS_reads < 0)
+ ? FLAGS_num
+ : ((FLAGS_writes > FLAGS_reads) ? FLAGS_writes : FLAGS_reads)),
+ merge_keys_(FLAGS_merge_keys < 0 ? FLAGS_num : FLAGS_merge_keys),
+ report_file_operations_(FLAGS_report_file_operations),
+#ifndef ROCKSDB_LITE
+ use_blob_db_(FLAGS_use_blob_db), // Stacked BlobDB
+#else
+ use_blob_db_(false), // Stacked BlobDB
+#endif // !ROCKSDB_LITE
+ read_operands_(false) {
+ // use simcache instead of cache
+ if (FLAGS_simcache_size >= 0) {
+ if (FLAGS_cache_numshardbits >= 1) {
+ cache_ =
+ NewSimCache(cache_, FLAGS_simcache_size, FLAGS_cache_numshardbits);
+ } else {
+ cache_ = NewSimCache(cache_, FLAGS_simcache_size, 0);
+ }
+ }
+
+ if (report_file_operations_) {
+ FLAGS_env = new CompositeEnvWrapper(
+ FLAGS_env,
+ std::make_shared<CountedFileSystem>(FLAGS_env->GetFileSystem()));
+ }
+
+ if (FLAGS_prefix_size > FLAGS_key_size) {
+ fprintf(stderr, "prefix size is larger than key size");
+ exit(1);
+ }
+
+ std::vector<std::string> files;
+ FLAGS_env->GetChildren(FLAGS_db, &files);
+ for (size_t i = 0; i < files.size(); i++) {
+ if (Slice(files[i]).starts_with("heap-")) {
+ FLAGS_env->DeleteFile(FLAGS_db + "/" + files[i]);
+ }
+ }
+ if (!FLAGS_use_existing_db) {
+ Options options;
+ options.env = FLAGS_env;
+ if (!FLAGS_wal_dir.empty()) {
+ options.wal_dir = FLAGS_wal_dir;
+ }
+#ifndef ROCKSDB_LITE
+ if (use_blob_db_) {
+ // Stacked BlobDB
+ blob_db::DestroyBlobDB(FLAGS_db, options, blob_db::BlobDBOptions());
+ }
+#endif // !ROCKSDB_LITE
+ DestroyDB(FLAGS_db, options);
+ if (!FLAGS_wal_dir.empty()) {
+ FLAGS_env->DeleteDir(FLAGS_wal_dir);
+ }
+
+ if (FLAGS_num_multi_db > 1) {
+ FLAGS_env->CreateDir(FLAGS_db);
+ if (!FLAGS_wal_dir.empty()) {
+ FLAGS_env->CreateDir(FLAGS_wal_dir);
+ }
+ }
+ }
+
+ listener_.reset(new ErrorHandlerListener());
+ if (user_timestamp_size_ > 0) {
+ mock_app_clock_.reset(new TimestampEmulator());
+ }
+ }
+
+ void DeleteDBs() {
+ db_.DeleteDBs();
+ for (const DBWithColumnFamilies& dbwcf : multi_dbs_) {
+ delete dbwcf.db;
+ }
+ }
+
+ ~Benchmark() {
+ DeleteDBs();
+ if (cache_.get() != nullptr) {
+ // Clear cache reference first
+ open_options_.write_buffer_manager.reset();
+ // this will leak, but we're shutting down so nobody cares
+ cache_->DisownData();
+ }
+ }
+
+ Slice AllocateKey(std::unique_ptr<const char[]>* key_guard) {
+ char* data = new char[key_size_];
+ const char* const_data = data;
+ key_guard->reset(const_data);
+ return Slice(key_guard->get(), key_size_);
+ }
+
+ // Generate key according to the given specification and random number.
+ // The resulting key will have the following format:
+ // - If keys_per_prefix_ is positive, extra trailing bytes are either cut
+ // off or padded with '0'.
+ // The prefix value is derived from key value.
+ // ----------------------------
+ // | prefix 00000 | key 00000 |
+ // ----------------------------
+ //
+ // - If keys_per_prefix_ is 0, the key is simply a binary representation of
+ // random number followed by trailing '0's
+ // ----------------------------
+ // | key 00000 |
+ // ----------------------------
+ void GenerateKeyFromInt(uint64_t v, int64_t num_keys, Slice* key) {
+ if (!keys_.empty()) {
+ assert(FLAGS_use_existing_keys);
+ assert(keys_.size() == static_cast<size_t>(num_keys));
+ assert(v < static_cast<uint64_t>(num_keys));
+ *key = keys_[v];
+ return;
+ }
+ char* start = const_cast<char*>(key->data());
+ char* pos = start;
+ if (keys_per_prefix_ > 0) {
+ int64_t num_prefix = num_keys / keys_per_prefix_;
+ int64_t prefix = v % num_prefix;
+ int bytes_to_fill = std::min(prefix_size_, 8);
+ if (port::kLittleEndian) {
+ for (int i = 0; i < bytes_to_fill; ++i) {
+ pos[i] = (prefix >> ((bytes_to_fill - i - 1) << 3)) & 0xFF;
+ }
+ } else {
+ memcpy(pos, static_cast<void*>(&prefix), bytes_to_fill);
+ }
+ if (prefix_size_ > 8) {
+ // fill the rest with 0s
+ memset(pos + 8, '0', prefix_size_ - 8);
+ }
+ pos += prefix_size_;
+ }
+
+ int bytes_to_fill = std::min(key_size_ - static_cast<int>(pos - start), 8);
+ if (port::kLittleEndian) {
+ for (int i = 0; i < bytes_to_fill; ++i) {
+ pos[i] = (v >> ((bytes_to_fill - i - 1) << 3)) & 0xFF;
+ }
+ } else {
+ memcpy(pos, static_cast<void*>(&v), bytes_to_fill);
+ }
+ pos += bytes_to_fill;
+ if (key_size_ > pos - start) {
+ memset(pos, '0', key_size_ - (pos - start));
+ }
+ }
+
+ void GenerateKeyFromIntForSeek(uint64_t v, int64_t num_keys, Slice* key) {
+ GenerateKeyFromInt(v, num_keys, key);
+ if (FLAGS_seek_missing_prefix) {
+ assert(prefix_size_ > 8);
+ char* key_ptr = const_cast<char*>(key->data());
+ // This rely on GenerateKeyFromInt filling paddings with '0's.
+ // Putting a '1' will create a non-existing prefix.
+ key_ptr[8] = '1';
+ }
+ }
+
+ std::string GetPathForMultiple(std::string base_name, size_t id) {
+ if (!base_name.empty()) {
+#ifndef OS_WIN
+ if (base_name.back() != '/') {
+ base_name += '/';
+ }
+#else
+ if (base_name.back() != '\\') {
+ base_name += '\\';
+ }
+#endif
+ }
+ return base_name + std::to_string(id);
+ }
+
+ void VerifyDBFromDB(std::string& truth_db_name) {
+ DBWithColumnFamilies truth_db;
+ auto s = DB::OpenForReadOnly(open_options_, truth_db_name, &truth_db.db);
+ if (!s.ok()) {
+ fprintf(stderr, "open error: %s\n", s.ToString().c_str());
+ exit(1);
+ }
+ ReadOptions ro;
+ ro.total_order_seek = true;
+ std::unique_ptr<Iterator> truth_iter(truth_db.db->NewIterator(ro));
+ std::unique_ptr<Iterator> db_iter(db_.db->NewIterator(ro));
+ // Verify that all the key/values in truth_db are retrivable in db with
+ // ::Get
+ fprintf(stderr, "Verifying db >= truth_db with ::Get...\n");
+ for (truth_iter->SeekToFirst(); truth_iter->Valid(); truth_iter->Next()) {
+ std::string value;
+ s = db_.db->Get(ro, truth_iter->key(), &value);
+ assert(s.ok());
+ // TODO(myabandeh): provide debugging hints
+ assert(Slice(value) == truth_iter->value());
+ }
+ // Verify that the db iterator does not give any extra key/value
+ fprintf(stderr, "Verifying db == truth_db...\n");
+ for (db_iter->SeekToFirst(), truth_iter->SeekToFirst(); db_iter->Valid();
+ db_iter->Next(), truth_iter->Next()) {
+ assert(truth_iter->Valid());
+ assert(truth_iter->value() == db_iter->value());
+ }
+ // No more key should be left unchecked in truth_db
+ assert(!truth_iter->Valid());
+ fprintf(stderr, "...Verified\n");
+ }
+
+ void ErrorExit() {
+ DeleteDBs();
+ exit(1);
+ }
+
+ void Run() {
+ if (!SanityCheck()) {
+ ErrorExit();
+ }
+ Open(&open_options_);
+ PrintHeader(open_options_);
+ std::stringstream benchmark_stream(FLAGS_benchmarks);
+ std::string name;
+ std::unique_ptr<ExpiredTimeFilter> filter;
+ while (std::getline(benchmark_stream, name, ',')) {
+ // Sanitize parameters
+ num_ = FLAGS_num;
+ reads_ = (FLAGS_reads < 0 ? FLAGS_num : FLAGS_reads);
+ writes_ = (FLAGS_writes < 0 ? FLAGS_num : FLAGS_writes);
+ deletes_ = (FLAGS_deletes < 0 ? FLAGS_num : FLAGS_deletes);
+ value_size = FLAGS_value_size;
+ key_size_ = FLAGS_key_size;
+ entries_per_batch_ = FLAGS_batch_size;
+ writes_before_delete_range_ = FLAGS_writes_before_delete_range;
+ writes_per_range_tombstone_ = FLAGS_writes_per_range_tombstone;
+ range_tombstone_width_ = FLAGS_range_tombstone_width;
+ max_num_range_tombstones_ = FLAGS_max_num_range_tombstones;
+ write_options_ = WriteOptions();
+ read_random_exp_range_ = FLAGS_read_random_exp_range;
+ if (FLAGS_sync) {
+ write_options_.sync = true;
+ }
+ write_options_.disableWAL = FLAGS_disable_wal;
+ write_options_.rate_limiter_priority =
+ FLAGS_rate_limit_auto_wal_flush ? Env::IO_USER : Env::IO_TOTAL;
+ read_options_ = ReadOptions(FLAGS_verify_checksum, true);
+ read_options_.total_order_seek = FLAGS_total_order_seek;
+ read_options_.prefix_same_as_start = FLAGS_prefix_same_as_start;
+ read_options_.rate_limiter_priority =
+ FLAGS_rate_limit_user_ops ? Env::IO_USER : Env::IO_TOTAL;
+ read_options_.tailing = FLAGS_use_tailing_iterator;
+ read_options_.readahead_size = FLAGS_readahead_size;
+ read_options_.adaptive_readahead = FLAGS_adaptive_readahead;
+ read_options_.async_io = FLAGS_async_io;
+ read_options_.optimize_multiget_for_io = FLAGS_optimize_multiget_for_io;
+
+ void (Benchmark::*method)(ThreadState*) = nullptr;
+ void (Benchmark::*post_process_method)() = nullptr;
+
+ bool fresh_db = false;
+ int num_threads = FLAGS_threads;
+
+ int num_repeat = 1;
+ int num_warmup = 0;
+ if (!name.empty() && *name.rbegin() == ']') {
+ auto it = name.find('[');
+ if (it == std::string::npos) {
+ fprintf(stderr, "unknown benchmark arguments '%s'\n", name.c_str());
+ ErrorExit();
+ }
+ std::string args = name.substr(it + 1);
+ args.resize(args.size() - 1);
+ name.resize(it);
+
+ std::string bench_arg;
+ std::stringstream args_stream(args);
+ while (std::getline(args_stream, bench_arg, '-')) {
+ if (bench_arg.empty()) {
+ continue;
+ }
+ if (bench_arg[0] == 'X') {
+ // Repeat the benchmark n times
+ std::string num_str = bench_arg.substr(1);
+ num_repeat = std::stoi(num_str);
+ } else if (bench_arg[0] == 'W') {
+ // Warm up the benchmark for n times
+ std::string num_str = bench_arg.substr(1);
+ num_warmup = std::stoi(num_str);
+ }
+ }
+ }
+
+ // Both fillseqdeterministic and filluniquerandomdeterministic
+ // fill the levels except the max level with UNIQUE_RANDOM
+ // and fill the max level with fillseq and filluniquerandom, respectively
+ if (name == "fillseqdeterministic" ||
+ name == "filluniquerandomdeterministic") {
+ if (!FLAGS_disable_auto_compactions) {
+ fprintf(stderr,
+ "Please disable_auto_compactions in FillDeterministic "
+ "benchmark\n");
+ ErrorExit();
+ }
+ if (num_threads > 1) {
+ fprintf(stderr,
+ "filldeterministic multithreaded not supported"
+ ", use 1 thread\n");
+ num_threads = 1;
+ }
+ fresh_db = true;
+ if (name == "fillseqdeterministic") {
+ method = &Benchmark::WriteSeqDeterministic;
+ } else {
+ method = &Benchmark::WriteUniqueRandomDeterministic;
+ }
+ } else if (name == "fillseq") {
+ fresh_db = true;
+ method = &Benchmark::WriteSeq;
+ } else if (name == "fillbatch") {
+ fresh_db = true;
+ entries_per_batch_ = 1000;
+ method = &Benchmark::WriteSeq;
+ } else if (name == "fillrandom") {
+ fresh_db = true;
+ method = &Benchmark::WriteRandom;
+ } else if (name == "filluniquerandom" ||
+ name == "fillanddeleteuniquerandom") {
+ fresh_db = true;
+ if (num_threads > 1) {
+ fprintf(stderr,
+ "filluniquerandom and fillanddeleteuniquerandom "
+ "multithreaded not supported, use 1 thread");
+ num_threads = 1;
+ }
+ method = &Benchmark::WriteUniqueRandom;
+ } else if (name == "overwrite") {
+ method = &Benchmark::WriteRandom;
+ } else if (name == "fillsync") {
+ fresh_db = true;
+ num_ /= 1000;
+ write_options_.sync = true;
+ method = &Benchmark::WriteRandom;
+ } else if (name == "fill100K") {
+ fresh_db = true;
+ num_ /= 1000;
+ value_size = 100 * 1000;
+ method = &Benchmark::WriteRandom;
+ } else if (name == "readseq") {
+ method = &Benchmark::ReadSequential;
+ } else if (name == "readtorowcache") {
+ if (!FLAGS_use_existing_keys || !FLAGS_row_cache_size) {
+ fprintf(stderr,
+ "Please set use_existing_keys to true and specify a "
+ "row cache size in readtorowcache benchmark\n");
+ ErrorExit();
+ }
+ method = &Benchmark::ReadToRowCache;
+ } else if (name == "readtocache") {
+ method = &Benchmark::ReadSequential;
+ num_threads = 1;
+ reads_ = num_;
+ } else if (name == "readreverse") {
+ method = &Benchmark::ReadReverse;
+ } else if (name == "readrandom") {
+ if (FLAGS_multiread_stride) {
+ fprintf(stderr, "entries_per_batch = %" PRIi64 "\n",
+ entries_per_batch_);
+ }
+ method = &Benchmark::ReadRandom;
+ } else if (name == "readrandomfast") {
+ method = &Benchmark::ReadRandomFast;
+ } else if (name == "multireadrandom") {
+ fprintf(stderr, "entries_per_batch = %" PRIi64 "\n",
+ entries_per_batch_);
+ method = &Benchmark::MultiReadRandom;
+ } else if (name == "multireadwhilewriting") {
+ fprintf(stderr, "entries_per_batch = %" PRIi64 "\n",
+ entries_per_batch_);
+ num_threads++;
+ method = &Benchmark::MultiReadWhileWriting;
+ } else if (name == "approximatesizerandom") {
+ fprintf(stderr, "entries_per_batch = %" PRIi64 "\n",
+ entries_per_batch_);
+ method = &Benchmark::ApproximateSizeRandom;
+ } else if (name == "mixgraph") {
+ method = &Benchmark::MixGraph;
+ } else if (name == "readmissing") {
+ ++key_size_;
+ method = &Benchmark::ReadRandom;
+ } else if (name == "newiterator") {
+ method = &Benchmark::IteratorCreation;
+ } else if (name == "newiteratorwhilewriting") {
+ num_threads++; // Add extra thread for writing
+ method = &Benchmark::IteratorCreationWhileWriting;
+ } else if (name == "seekrandom") {
+ method = &Benchmark::SeekRandom;
+ } else if (name == "seekrandomwhilewriting") {
+ num_threads++; // Add extra thread for writing
+ method = &Benchmark::SeekRandomWhileWriting;
+ } else if (name == "seekrandomwhilemerging") {
+ num_threads++; // Add extra thread for merging
+ method = &Benchmark::SeekRandomWhileMerging;
+ } else if (name == "readrandomsmall") {
+ reads_ /= 1000;
+ method = &Benchmark::ReadRandom;
+ } else if (name == "deleteseq") {
+ method = &Benchmark::DeleteSeq;
+ } else if (name == "deleterandom") {
+ method = &Benchmark::DeleteRandom;
+ } else if (name == "readwhilewriting") {
+ num_threads++; // Add extra thread for writing
+ method = &Benchmark::ReadWhileWriting;
+ } else if (name == "readwhilemerging") {
+ num_threads++; // Add extra thread for writing
+ method = &Benchmark::ReadWhileMerging;
+ } else if (name == "readwhilescanning") {
+ num_threads++; // Add extra thread for scaning
+ method = &Benchmark::ReadWhileScanning;
+ } else if (name == "readrandomwriterandom") {
+ method = &Benchmark::ReadRandomWriteRandom;
+ } else if (name == "readrandommergerandom") {
+ if (FLAGS_merge_operator.empty()) {
+ fprintf(stdout, "%-12s : skipped (--merge_operator is unknown)\n",
+ name.c_str());
+ ErrorExit();
+ }
+ method = &Benchmark::ReadRandomMergeRandom;
+ } else if (name == "updaterandom") {
+ method = &Benchmark::UpdateRandom;
+ } else if (name == "xorupdaterandom") {
+ method = &Benchmark::XORUpdateRandom;
+ } else if (name == "appendrandom") {
+ method = &Benchmark::AppendRandom;
+ } else if (name == "mergerandom") {
+ if (FLAGS_merge_operator.empty()) {
+ fprintf(stdout, "%-12s : skipped (--merge_operator is unknown)\n",
+ name.c_str());
+ exit(1);
+ }
+ method = &Benchmark::MergeRandom;
+ } else if (name == "randomwithverify") {
+ method = &Benchmark::RandomWithVerify;
+ } else if (name == "fillseekseq") {
+ method = &Benchmark::WriteSeqSeekSeq;
+ } else if (name == "compact") {
+ method = &Benchmark::Compact;
+ } else if (name == "compactall") {
+ CompactAll();
+#ifndef ROCKSDB_LITE
+ } else if (name == "compact0") {
+ CompactLevel(0);
+ } else if (name == "compact1") {
+ CompactLevel(1);
+ } else if (name == "waitforcompaction") {
+ WaitForCompaction();
+#endif
+ } else if (name == "flush") {
+ Flush();
+ } else if (name == "crc32c") {
+ method = &Benchmark::Crc32c;
+ } else if (name == "xxhash") {
+ method = &Benchmark::xxHash;
+ } else if (name == "xxhash64") {
+ method = &Benchmark::xxHash64;
+ } else if (name == "xxh3") {
+ method = &Benchmark::xxh3;
+ } else if (name == "acquireload") {
+ method = &Benchmark::AcquireLoad;
+ } else if (name == "compress") {
+ method = &Benchmark::Compress;
+ } else if (name == "uncompress") {
+ method = &Benchmark::Uncompress;
+#ifndef ROCKSDB_LITE
+ } else if (name == "randomtransaction") {
+ method = &Benchmark::RandomTransaction;
+ post_process_method = &Benchmark::RandomTransactionVerify;
+#endif // ROCKSDB_LITE
+ } else if (name == "randomreplacekeys") {
+ fresh_db = true;
+ method = &Benchmark::RandomReplaceKeys;
+ } else if (name == "timeseries") {
+ timestamp_emulator_.reset(new TimestampEmulator());
+ if (FLAGS_expire_style == "compaction_filter") {
+ filter.reset(new ExpiredTimeFilter(timestamp_emulator_));
+ fprintf(stdout, "Compaction filter is used to remove expired data");
+ open_options_.compaction_filter = filter.get();
+ }
+ fresh_db = true;
+ method = &Benchmark::TimeSeries;
+ } else if (name == "block_cache_entry_stats") {
+ // DB::Properties::kBlockCacheEntryStats
+ PrintStats("rocksdb.block-cache-entry-stats");
+ } else if (name == "stats") {
+ PrintStats("rocksdb.stats");
+ } else if (name == "resetstats") {
+ ResetStats();
+ } else if (name == "verify") {
+ VerifyDBFromDB(FLAGS_truth_db);
+ } else if (name == "levelstats") {
+ PrintStats("rocksdb.levelstats");
+ } else if (name == "memstats") {
+ std::vector<std::string> keys{"rocksdb.num-immutable-mem-table",
+ "rocksdb.cur-size-active-mem-table",
+ "rocksdb.cur-size-all-mem-tables",
+ "rocksdb.size-all-mem-tables",
+ "rocksdb.num-entries-active-mem-table",
+ "rocksdb.num-entries-imm-mem-tables"};
+ PrintStats(keys);
+ } else if (name == "sstables") {
+ PrintStats("rocksdb.sstables");
+ } else if (name == "stats_history") {
+ PrintStatsHistory();
+#ifndef ROCKSDB_LITE
+ } else if (name == "replay") {
+ if (num_threads > 1) {
+ fprintf(stderr, "Multi-threaded replay is not yet supported\n");
+ ErrorExit();
+ }
+ if (FLAGS_trace_file == "") {
+ fprintf(stderr, "Please set --trace_file to be replayed from\n");
+ ErrorExit();
+ }
+ method = &Benchmark::Replay;
+#endif // ROCKSDB_LITE
+ } else if (name == "getmergeoperands") {
+ method = &Benchmark::GetMergeOperands;
+#ifndef ROCKSDB_LITE
+ } else if (name == "verifychecksum") {
+ method = &Benchmark::VerifyChecksum;
+ } else if (name == "verifyfilechecksums") {
+ method = &Benchmark::VerifyFileChecksums;
+#endif // ROCKSDB_LITE
+ } else if (name == "readrandomoperands") {
+ read_operands_ = true;
+ method = &Benchmark::ReadRandom;
+#ifndef ROCKSDB_LITE
+ } else if (name == "backup") {
+ method = &Benchmark::Backup;
+ } else if (name == "restore") {
+ method = &Benchmark::Restore;
+#endif
+ } else if (!name.empty()) { // No error message for empty name
+ fprintf(stderr, "unknown benchmark '%s'\n", name.c_str());
+ ErrorExit();
+ }
+
+ if (fresh_db) {
+ if (FLAGS_use_existing_db) {
+ fprintf(stdout, "%-12s : skipped (--use_existing_db is true)\n",
+ name.c_str());
+ method = nullptr;
+ } else {
+ if (db_.db != nullptr) {
+ db_.DeleteDBs();
+ DestroyDB(FLAGS_db, open_options_);
+ }
+ Options options = open_options_;
+ for (size_t i = 0; i < multi_dbs_.size(); i++) {
+ delete multi_dbs_[i].db;
+ if (!open_options_.wal_dir.empty()) {
+ options.wal_dir = GetPathForMultiple(open_options_.wal_dir, i);
+ }
+ DestroyDB(GetPathForMultiple(FLAGS_db, i), options);
+ }
+ multi_dbs_.clear();
+ }
+ Open(&open_options_); // use open_options for the last accessed
+ }
+
+ if (method != nullptr) {
+ fprintf(stdout, "DB path: [%s]\n", FLAGS_db.c_str());
+
+#ifndef ROCKSDB_LITE
+ if (name == "backup") {
+ std::cout << "Backup path: [" << FLAGS_backup_dir << "]" << std::endl;
+ } else if (name == "restore") {
+ std::cout << "Backup path: [" << FLAGS_backup_dir << "]" << std::endl;
+ std::cout << "Restore path: [" << FLAGS_restore_dir << "]"
+ << std::endl;
+ }
+ // A trace_file option can be provided both for trace and replay
+ // operations. But db_bench does not support tracing and replaying at
+ // the same time, for now. So, start tracing only when it is not a
+ // replay.
+ if (FLAGS_trace_file != "" && name != "replay") {
+ std::unique_ptr<TraceWriter> trace_writer;
+ Status s = NewFileTraceWriter(FLAGS_env, EnvOptions(),
+ FLAGS_trace_file, &trace_writer);
+ if (!s.ok()) {
+ fprintf(stderr, "Encountered an error starting a trace, %s\n",
+ s.ToString().c_str());
+ ErrorExit();
+ }
+ s = db_.db->StartTrace(trace_options_, std::move(trace_writer));
+ if (!s.ok()) {
+ fprintf(stderr, "Encountered an error starting a trace, %s\n",
+ s.ToString().c_str());
+ ErrorExit();
+ }
+ fprintf(stdout, "Tracing the workload to: [%s]\n",
+ FLAGS_trace_file.c_str());
+ }
+ // Start block cache tracing.
+ if (!FLAGS_block_cache_trace_file.empty()) {
+ // Sanity checks.
+ if (FLAGS_block_cache_trace_sampling_frequency <= 0) {
+ fprintf(stderr,
+ "Block cache trace sampling frequency must be higher than "
+ "0.\n");
+ ErrorExit();
+ }
+ if (FLAGS_block_cache_trace_max_trace_file_size_in_bytes <= 0) {
+ fprintf(stderr,
+ "The maximum file size for block cache tracing must be "
+ "higher than 0.\n");
+ ErrorExit();
+ }
+ block_cache_trace_options_.max_trace_file_size =
+ FLAGS_block_cache_trace_max_trace_file_size_in_bytes;
+ block_cache_trace_options_.sampling_frequency =
+ FLAGS_block_cache_trace_sampling_frequency;
+ std::unique_ptr<TraceWriter> block_cache_trace_writer;
+ Status s = NewFileTraceWriter(FLAGS_env, EnvOptions(),
+ FLAGS_block_cache_trace_file,
+ &block_cache_trace_writer);
+ if (!s.ok()) {
+ fprintf(stderr,
+ "Encountered an error when creating trace writer, %s\n",
+ s.ToString().c_str());
+ ErrorExit();
+ }
+ s = db_.db->StartBlockCacheTrace(block_cache_trace_options_,
+ std::move(block_cache_trace_writer));
+ if (!s.ok()) {
+ fprintf(
+ stderr,
+ "Encountered an error when starting block cache tracing, %s\n",
+ s.ToString().c_str());
+ ErrorExit();
+ }
+ fprintf(stdout, "Tracing block cache accesses to: [%s]\n",
+ FLAGS_block_cache_trace_file.c_str());
+ }
+#endif // ROCKSDB_LITE
+
+ if (num_warmup > 0) {
+ printf("Warming up benchmark by running %d times\n", num_warmup);
+ }
+
+ for (int i = 0; i < num_warmup; i++) {
+ RunBenchmark(num_threads, name, method);
+ }
+
+ if (num_repeat > 1) {
+ printf("Running benchmark for %d times\n", num_repeat);
+ }
+
+ CombinedStats combined_stats;
+ for (int i = 0; i < num_repeat; i++) {
+ Stats stats = RunBenchmark(num_threads, name, method);
+ combined_stats.AddStats(stats);
+ if (FLAGS_confidence_interval_only) {
+ combined_stats.ReportWithConfidenceIntervals(name);
+ } else {
+ combined_stats.Report(name);
+ }
+ }
+ if (num_repeat > 1) {
+ combined_stats.ReportFinal(name);
+ }
+ }
+ if (post_process_method != nullptr) {
+ (this->*post_process_method)();
+ }
+ }
+
+ if (secondary_update_thread_) {
+ secondary_update_stopped_.store(1, std::memory_order_relaxed);
+ secondary_update_thread_->join();
+ secondary_update_thread_.reset();
+ }
+
+#ifndef ROCKSDB_LITE
+ if (name != "replay" && FLAGS_trace_file != "") {
+ Status s = db_.db->EndTrace();
+ if (!s.ok()) {
+ fprintf(stderr, "Encountered an error ending the trace, %s\n",
+ s.ToString().c_str());
+ }
+ }
+ if (!FLAGS_block_cache_trace_file.empty()) {
+ Status s = db_.db->EndBlockCacheTrace();
+ if (!s.ok()) {
+ fprintf(stderr,
+ "Encountered an error ending the block cache tracing, %s\n",
+ s.ToString().c_str());
+ }
+ }
+#endif // ROCKSDB_LITE
+
+ if (FLAGS_statistics) {
+ fprintf(stdout, "STATISTICS:\n%s\n", dbstats->ToString().c_str());
+ }
+ if (FLAGS_simcache_size >= 0) {
+ fprintf(
+ stdout, "SIMULATOR CACHE STATISTICS:\n%s\n",
+ static_cast_with_check<SimCache>(cache_.get())->ToString().c_str());
+ }
+
+#ifndef ROCKSDB_LITE
+ if (FLAGS_use_secondary_db) {
+ fprintf(stdout, "Secondary instance updated %" PRIu64 " times.\n",
+ secondary_db_updates_);
+ }
+#endif // ROCKSDB_LITE
+ }
+
+ private:
+ std::shared_ptr<TimestampEmulator> timestamp_emulator_;
+ std::unique_ptr<port::Thread> secondary_update_thread_;
+ std::atomic<int> secondary_update_stopped_{0};
+#ifndef ROCKSDB_LITE
+ uint64_t secondary_db_updates_ = 0;
+#endif // ROCKSDB_LITE
+ struct ThreadArg {
+ Benchmark* bm;
+ SharedState* shared;
+ ThreadState* thread;
+ void (Benchmark::*method)(ThreadState*);
+ };
+
+ static void ThreadBody(void* v) {
+ ThreadArg* arg = reinterpret_cast<ThreadArg*>(v);
+ SharedState* shared = arg->shared;
+ ThreadState* thread = arg->thread;
+ {
+ MutexLock l(&shared->mu);
+ shared->num_initialized++;
+ if (shared->num_initialized >= shared->total) {
+ shared->cv.SignalAll();
+ }
+ while (!shared->start) {
+ shared->cv.Wait();
+ }
+ }
+
+ SetPerfLevel(static_cast<PerfLevel>(shared->perf_level));
+ perf_context.EnablePerLevelPerfContext();
+ thread->stats.Start(thread->tid);
+ (arg->bm->*(arg->method))(thread);
+ if (FLAGS_perf_level > ROCKSDB_NAMESPACE::PerfLevel::kDisable) {
+ thread->stats.AddMessage(std::string("PERF_CONTEXT:\n") +
+ get_perf_context()->ToString());
+ }
+ thread->stats.Stop();
+
+ {
+ MutexLock l(&shared->mu);
+ shared->num_done++;
+ if (shared->num_done >= shared->total) {
+ shared->cv.SignalAll();
+ }
+ }
+ }
+
+ Stats RunBenchmark(int n, Slice name,
+ void (Benchmark::*method)(ThreadState*)) {
+ SharedState shared;
+ shared.total = n;
+ shared.num_initialized = 0;
+ shared.num_done = 0;
+ shared.start = false;
+ if (FLAGS_benchmark_write_rate_limit > 0) {
+ shared.write_rate_limiter.reset(
+ NewGenericRateLimiter(FLAGS_benchmark_write_rate_limit));
+ }
+ if (FLAGS_benchmark_read_rate_limit > 0) {
+ shared.read_rate_limiter.reset(NewGenericRateLimiter(
+ FLAGS_benchmark_read_rate_limit, 100000 /* refill_period_us */,
+ 10 /* fairness */, RateLimiter::Mode::kReadsOnly));
+ }
+
+ std::unique_ptr<ReporterAgent> reporter_agent;
+ if (FLAGS_report_interval_seconds > 0) {
+ reporter_agent.reset(new ReporterAgent(FLAGS_env, FLAGS_report_file,
+ FLAGS_report_interval_seconds));
+ }
+
+ ThreadArg* arg = new ThreadArg[n];
+
+ for (int i = 0; i < n; i++) {
+#ifdef NUMA
+ if (FLAGS_enable_numa) {
+ // Performs a local allocation of memory to threads in numa node.
+ int n_nodes = numa_num_task_nodes(); // Number of nodes in NUMA.
+ numa_exit_on_error = 1;
+ int numa_node = i % n_nodes;
+ bitmask* nodes = numa_allocate_nodemask();
+ numa_bitmask_clearall(nodes);
+ numa_bitmask_setbit(nodes, numa_node);
+ // numa_bind() call binds the process to the node and these
+ // properties are passed on to the thread that is created in
+ // StartThread method called later in the loop.
+ numa_bind(nodes);
+ numa_set_strict(1);
+ numa_free_nodemask(nodes);
+ }
+#endif
+ arg[i].bm = this;
+ arg[i].method = method;
+ arg[i].shared = &shared;
+ total_thread_count_++;
+ arg[i].thread = new ThreadState(i, total_thread_count_);
+ arg[i].thread->stats.SetReporterAgent(reporter_agent.get());
+ arg[i].thread->shared = &shared;
+ FLAGS_env->StartThread(ThreadBody, &arg[i]);
+ }
+
+ shared.mu.Lock();
+ while (shared.num_initialized < n) {
+ shared.cv.Wait();
+ }
+
+ shared.start = true;
+ shared.cv.SignalAll();
+ while (shared.num_done < n) {
+ shared.cv.Wait();
+ }
+ shared.mu.Unlock();
+
+ // Stats for some threads can be excluded.
+ Stats merge_stats;
+ for (int i = 0; i < n; i++) {
+ merge_stats.Merge(arg[i].thread->stats);
+ }
+ merge_stats.Report(name);
+
+ for (int i = 0; i < n; i++) {
+ delete arg[i].thread;
+ }
+ delete[] arg;
+
+ return merge_stats;
+ }
+
+ template <OperationType kOpType, typename FnType, typename... Args>
+ static inline void ChecksumBenchmark(FnType fn, ThreadState* thread,
+ Args... args) {
+ const int size = FLAGS_block_size; // use --block_size option for db_bench
+ std::string labels = "(" + std::to_string(FLAGS_block_size) + " per op)";
+ const char* label = labels.c_str();
+
+ std::string data(size, 'x');
+ uint64_t bytes = 0;
+ uint32_t val = 0;
+ while (bytes < 5000U * uint64_t{1048576}) { // ~5GB
+ val += static_cast<uint32_t>(fn(data.data(), size, args...));
+ thread->stats.FinishedOps(nullptr, nullptr, 1, kOpType);
+ bytes += size;
+ }
+ // Print so result is not dead
+ fprintf(stderr, "... val=0x%x\r", static_cast<unsigned int>(val));
+
+ thread->stats.AddBytes(bytes);
+ thread->stats.AddMessage(label);
+ }
+
+ void Crc32c(ThreadState* thread) {
+ ChecksumBenchmark<kCrc>(crc32c::Value, thread);
+ }
+
+ void xxHash(ThreadState* thread) {
+ ChecksumBenchmark<kHash>(XXH32, thread, /*seed*/ 0);
+ }
+
+ void xxHash64(ThreadState* thread) {
+ ChecksumBenchmark<kHash>(XXH64, thread, /*seed*/ 0);
+ }
+
+ void xxh3(ThreadState* thread) {
+ ChecksumBenchmark<kHash>(XXH3_64bits, thread);
+ }
+
+ void AcquireLoad(ThreadState* thread) {
+ int dummy;
+ std::atomic<void*> ap(&dummy);
+ int count = 0;
+ void* ptr = nullptr;
+ thread->stats.AddMessage("(each op is 1000 loads)");
+ while (count < 100000) {
+ for (int i = 0; i < 1000; i++) {
+ ptr = ap.load(std::memory_order_acquire);
+ }
+ count++;
+ thread->stats.FinishedOps(nullptr, nullptr, 1, kOthers);
+ }
+ if (ptr == nullptr) exit(1); // Disable unused variable warning.
+ }
+
+ void Compress(ThreadState* thread) {
+ RandomGenerator gen;
+ Slice input = gen.Generate(FLAGS_block_size);
+ int64_t bytes = 0;
+ int64_t produced = 0;
+ bool ok = true;
+ std::string compressed;
+ CompressionOptions opts;
+ CompressionContext context(FLAGS_compression_type_e);
+ CompressionInfo info(opts, context, CompressionDict::GetEmptyDict(),
+ FLAGS_compression_type_e,
+ FLAGS_sample_for_compression);
+ // Compress 1G
+ while (ok && bytes < int64_t(1) << 30) {
+ compressed.clear();
+ ok = CompressSlice(info, input, &compressed);
+ produced += compressed.size();
+ bytes += input.size();
+ thread->stats.FinishedOps(nullptr, nullptr, 1, kCompress);
+ }
+
+ if (!ok) {
+ thread->stats.AddMessage("(compression failure)");
+ } else {
+ char buf[340];
+ snprintf(buf, sizeof(buf), "(output: %.1f%%)",
+ (produced * 100.0) / bytes);
+ thread->stats.AddMessage(buf);
+ thread->stats.AddBytes(bytes);
+ }
+ }
+
+ void Uncompress(ThreadState* thread) {
+ RandomGenerator gen;
+ Slice input = gen.Generate(FLAGS_block_size);
+ std::string compressed;
+
+ CompressionContext compression_ctx(FLAGS_compression_type_e);
+ CompressionOptions compression_opts;
+ CompressionInfo compression_info(
+ compression_opts, compression_ctx, CompressionDict::GetEmptyDict(),
+ FLAGS_compression_type_e, FLAGS_sample_for_compression);
+ UncompressionContext uncompression_ctx(FLAGS_compression_type_e);
+ UncompressionInfo uncompression_info(uncompression_ctx,
+ UncompressionDict::GetEmptyDict(),
+ FLAGS_compression_type_e);
+
+ bool ok = CompressSlice(compression_info, input, &compressed);
+ int64_t bytes = 0;
+ size_t uncompressed_size = 0;
+ while (ok && bytes < 1024 * 1048576) {
+ constexpr uint32_t compress_format_version = 2;
+
+ CacheAllocationPtr uncompressed = UncompressData(
+ uncompression_info, compressed.data(), compressed.size(),
+ &uncompressed_size, compress_format_version);
+
+ ok = uncompressed.get() != nullptr;
+ bytes += input.size();
+ thread->stats.FinishedOps(nullptr, nullptr, 1, kUncompress);
+ }
+
+ if (!ok) {
+ thread->stats.AddMessage("(compression failure)");
+ } else {
+ thread->stats.AddBytes(bytes);
+ }
+ }
+
+ // Returns true if the options is initialized from the specified
+ // options file.
+ bool InitializeOptionsFromFile(Options* opts) {
+#ifndef ROCKSDB_LITE
+ printf("Initializing RocksDB Options from the specified file\n");
+ DBOptions db_opts;
+ std::vector<ColumnFamilyDescriptor> cf_descs;
+ if (FLAGS_options_file != "") {
+ auto s = LoadOptionsFromFile(FLAGS_options_file, FLAGS_env, &db_opts,
+ &cf_descs);
+ db_opts.env = FLAGS_env;
+ if (s.ok()) {
+ *opts = Options(db_opts, cf_descs[0].options);
+ return true;
+ }
+ fprintf(stderr, "Unable to load options file %s --- %s\n",
+ FLAGS_options_file.c_str(), s.ToString().c_str());
+ exit(1);
+ }
+#else
+ (void)opts;
+#endif
+ return false;
+ }
+
+ void InitializeOptionsFromFlags(Options* opts) {
+ printf("Initializing RocksDB Options from command-line flags\n");
+ Options& options = *opts;
+ ConfigOptions config_options(options);
+ config_options.ignore_unsupported_options = false;
+
+ assert(db_.db == nullptr);
+
+ options.env = FLAGS_env;
+ options.wal_dir = FLAGS_wal_dir;
+ options.dump_malloc_stats = FLAGS_dump_malloc_stats;
+ options.stats_dump_period_sec =
+ static_cast<unsigned int>(FLAGS_stats_dump_period_sec);
+ options.stats_persist_period_sec =
+ static_cast<unsigned int>(FLAGS_stats_persist_period_sec);
+ options.persist_stats_to_disk = FLAGS_persist_stats_to_disk;
+ options.stats_history_buffer_size =
+ static_cast<size_t>(FLAGS_stats_history_buffer_size);
+ options.avoid_flush_during_recovery = FLAGS_avoid_flush_during_recovery;
+
+ options.compression_opts.level = FLAGS_compression_level;
+ options.compression_opts.max_dict_bytes = FLAGS_compression_max_dict_bytes;
+ options.compression_opts.zstd_max_train_bytes =
+ FLAGS_compression_zstd_max_train_bytes;
+ options.compression_opts.parallel_threads =
+ FLAGS_compression_parallel_threads;
+ options.compression_opts.max_dict_buffer_bytes =
+ FLAGS_compression_max_dict_buffer_bytes;
+ options.compression_opts.use_zstd_dict_trainer =
+ FLAGS_compression_use_zstd_dict_trainer;
+
+ options.max_open_files = FLAGS_open_files;
+ if (FLAGS_cost_write_buffer_to_cache || FLAGS_db_write_buffer_size != 0) {
+ options.write_buffer_manager.reset(
+ new WriteBufferManager(FLAGS_db_write_buffer_size, cache_));
+ }
+ options.arena_block_size = FLAGS_arena_block_size;
+ options.write_buffer_size = FLAGS_write_buffer_size;
+ options.max_write_buffer_number = FLAGS_max_write_buffer_number;
+ options.min_write_buffer_number_to_merge =
+ FLAGS_min_write_buffer_number_to_merge;
+ options.max_write_buffer_number_to_maintain =
+ FLAGS_max_write_buffer_number_to_maintain;
+ options.max_write_buffer_size_to_maintain =
+ FLAGS_max_write_buffer_size_to_maintain;
+ options.max_background_jobs = FLAGS_max_background_jobs;
+ options.max_background_compactions = FLAGS_max_background_compactions;
+ options.max_subcompactions = static_cast<uint32_t>(FLAGS_subcompactions);
+ options.max_background_flushes = FLAGS_max_background_flushes;
+ options.compaction_style = FLAGS_compaction_style_e;
+ options.compaction_pri = FLAGS_compaction_pri_e;
+ options.allow_mmap_reads = FLAGS_mmap_read;
+ options.allow_mmap_writes = FLAGS_mmap_write;
+ options.use_direct_reads = FLAGS_use_direct_reads;
+ options.use_direct_io_for_flush_and_compaction =
+ FLAGS_use_direct_io_for_flush_and_compaction;
+ options.manual_wal_flush = FLAGS_manual_wal_flush;
+ options.wal_compression = FLAGS_wal_compression_e;
+#ifndef ROCKSDB_LITE
+ options.ttl = FLAGS_fifo_compaction_ttl;
+ options.compaction_options_fifo = CompactionOptionsFIFO(
+ FLAGS_fifo_compaction_max_table_files_size_mb * 1024 * 1024,
+ FLAGS_fifo_compaction_allow_compaction);
+ options.compaction_options_fifo.age_for_warm = FLAGS_fifo_age_for_warm;
+#endif // ROCKSDB_LITE
+ options.prefix_extractor = prefix_extractor_;
+ if (FLAGS_use_uint64_comparator) {
+ options.comparator = test::Uint64Comparator();
+ if (FLAGS_key_size != 8) {
+ fprintf(stderr, "Using Uint64 comparator but key size is not 8.\n");
+ exit(1);
+ }
+ }
+ if (FLAGS_use_stderr_info_logger) {
+ options.info_log.reset(new StderrLogger());
+ }
+ options.memtable_huge_page_size = FLAGS_memtable_use_huge_page ? 2048 : 0;
+ options.memtable_prefix_bloom_size_ratio = FLAGS_memtable_bloom_size_ratio;
+ options.memtable_whole_key_filtering = FLAGS_memtable_whole_key_filtering;
+ if (FLAGS_memtable_insert_with_hint_prefix_size > 0) {
+ options.memtable_insert_with_hint_prefix_extractor.reset(
+ NewCappedPrefixTransform(
+ FLAGS_memtable_insert_with_hint_prefix_size));
+ }
+ options.bloom_locality = FLAGS_bloom_locality;
+ options.max_file_opening_threads = FLAGS_file_opening_threads;
+ options.compaction_readahead_size = FLAGS_compaction_readahead_size;
+ options.log_readahead_size = FLAGS_log_readahead_size;
+ options.random_access_max_buffer_size = FLAGS_random_access_max_buffer_size;
+ options.writable_file_max_buffer_size = FLAGS_writable_file_max_buffer_size;
+ options.use_fsync = FLAGS_use_fsync;
+ options.num_levels = FLAGS_num_levels;
+ options.target_file_size_base = FLAGS_target_file_size_base;
+ options.target_file_size_multiplier = FLAGS_target_file_size_multiplier;
+ options.max_bytes_for_level_base = FLAGS_max_bytes_for_level_base;
+ options.level_compaction_dynamic_level_bytes =
+ FLAGS_level_compaction_dynamic_level_bytes;
+ options.max_bytes_for_level_multiplier =
+ FLAGS_max_bytes_for_level_multiplier;
+ Status s =
+ CreateMemTableRepFactory(config_options, &options.memtable_factory);
+ if (!s.ok()) {
+ fprintf(stderr, "Could not create memtable factory: %s\n",
+ s.ToString().c_str());
+ exit(1);
+ } else if ((FLAGS_prefix_size == 0) &&
+ (options.memtable_factory->IsInstanceOf("prefix_hash") ||
+ options.memtable_factory->IsInstanceOf("hash_linkedlist"))) {
+ fprintf(stderr,
+ "prefix_size should be non-zero if PrefixHash or "
+ "HashLinkedList memtablerep is used\n");
+ exit(1);
+ }
+ if (FLAGS_use_plain_table) {
+#ifndef ROCKSDB_LITE
+ if (!options.memtable_factory->IsInstanceOf("prefix_hash") &&
+ !options.memtable_factory->IsInstanceOf("hash_linkedlist")) {
+ fprintf(stderr, "Warning: plain table is used with %s\n",
+ options.memtable_factory->Name());
+ }
+
+ int bloom_bits_per_key = FLAGS_bloom_bits;
+ if (bloom_bits_per_key < 0) {
+ bloom_bits_per_key = PlainTableOptions().bloom_bits_per_key;
+ }
+
+ PlainTableOptions plain_table_options;
+ plain_table_options.user_key_len = FLAGS_key_size;
+ plain_table_options.bloom_bits_per_key = bloom_bits_per_key;
+ plain_table_options.hash_table_ratio = 0.75;
+ options.table_factory = std::shared_ptr<TableFactory>(
+ NewPlainTableFactory(plain_table_options));
+#else
+ fprintf(stderr, "Plain table is not supported in lite mode\n");
+ exit(1);
+#endif // ROCKSDB_LITE
+ } else if (FLAGS_use_cuckoo_table) {
+#ifndef ROCKSDB_LITE
+ if (FLAGS_cuckoo_hash_ratio > 1 || FLAGS_cuckoo_hash_ratio < 0) {
+ fprintf(stderr, "Invalid cuckoo_hash_ratio\n");
+ exit(1);
+ }
+
+ if (!FLAGS_mmap_read) {
+ fprintf(stderr, "cuckoo table format requires mmap read to operate\n");
+ exit(1);
+ }
+
+ ROCKSDB_NAMESPACE::CuckooTableOptions table_options;
+ table_options.hash_table_ratio = FLAGS_cuckoo_hash_ratio;
+ table_options.identity_as_first_hash = FLAGS_identity_as_first_hash;
+ options.table_factory =
+ std::shared_ptr<TableFactory>(NewCuckooTableFactory(table_options));
+#else
+ fprintf(stderr, "Cuckoo table is not supported in lite mode\n");
+ exit(1);
+#endif // ROCKSDB_LITE
+ } else {
+ BlockBasedTableOptions block_based_options;
+ block_based_options.checksum =
+ static_cast<ChecksumType>(FLAGS_checksum_type);
+ if (FLAGS_use_hash_search) {
+ if (FLAGS_prefix_size == 0) {
+ fprintf(stderr,
+ "prefix_size not assigned when enable use_hash_search \n");
+ exit(1);
+ }
+ block_based_options.index_type = BlockBasedTableOptions::kHashSearch;
+ } else {
+ block_based_options.index_type = BlockBasedTableOptions::kBinarySearch;
+ }
+ if (FLAGS_partition_index_and_filters || FLAGS_partition_index) {
+ if (FLAGS_index_with_first_key) {
+ fprintf(stderr,
+ "--index_with_first_key is not compatible with"
+ " partition index.");
+ }
+ if (FLAGS_use_hash_search) {
+ fprintf(stderr,
+ "use_hash_search is incompatible with "
+ "partition index and is ignored");
+ }
+ block_based_options.index_type =
+ BlockBasedTableOptions::kTwoLevelIndexSearch;
+ block_based_options.metadata_block_size = FLAGS_metadata_block_size;
+ if (FLAGS_partition_index_and_filters) {
+ block_based_options.partition_filters = true;
+ }
+ } else if (FLAGS_index_with_first_key) {
+ block_based_options.index_type =
+ BlockBasedTableOptions::kBinarySearchWithFirstKey;
+ }
+ BlockBasedTableOptions::IndexShorteningMode index_shortening =
+ block_based_options.index_shortening;
+ switch (FLAGS_index_shortening_mode) {
+ case 0:
+ index_shortening =
+ BlockBasedTableOptions::IndexShorteningMode::kNoShortening;
+ break;
+ case 1:
+ index_shortening =
+ BlockBasedTableOptions::IndexShorteningMode::kShortenSeparators;
+ break;
+ case 2:
+ index_shortening = BlockBasedTableOptions::IndexShorteningMode::
+ kShortenSeparatorsAndSuccessor;
+ break;
+ default:
+ fprintf(stderr, "Unknown key shortening mode\n");
+ }
+ block_based_options.optimize_filters_for_memory =
+ FLAGS_optimize_filters_for_memory;
+ block_based_options.index_shortening = index_shortening;
+ if (cache_ == nullptr) {
+ block_based_options.no_block_cache = true;
+ }
+ block_based_options.cache_index_and_filter_blocks =
+ FLAGS_cache_index_and_filter_blocks;
+ block_based_options.pin_l0_filter_and_index_blocks_in_cache =
+ FLAGS_pin_l0_filter_and_index_blocks_in_cache;
+ block_based_options.pin_top_level_index_and_filter =
+ FLAGS_pin_top_level_index_and_filter;
+ if (FLAGS_cache_high_pri_pool_ratio > 1e-6) { // > 0.0 + eps
+ block_based_options.cache_index_and_filter_blocks_with_high_priority =
+ true;
+ }
+ if (FLAGS_cache_high_pri_pool_ratio + FLAGS_cache_low_pri_pool_ratio >
+ 1.0) {
+ fprintf(stderr,
+ "Sum of high_pri_pool_ratio and low_pri_pool_ratio "
+ "cannot exceed 1.0.\n");
+ }
+ block_based_options.block_cache = cache_;
+ block_based_options.cache_usage_options.options_overrides.insert(
+ {CacheEntryRole::kCompressionDictionaryBuildingBuffer,
+ {/*.charged = */ FLAGS_charge_compression_dictionary_building_buffer
+ ? CacheEntryRoleOptions::Decision::kEnabled
+ : CacheEntryRoleOptions::Decision::kDisabled}});
+ block_based_options.cache_usage_options.options_overrides.insert(
+ {CacheEntryRole::kFilterConstruction,
+ {/*.charged = */ FLAGS_charge_filter_construction
+ ? CacheEntryRoleOptions::Decision::kEnabled
+ : CacheEntryRoleOptions::Decision::kDisabled}});
+ block_based_options.cache_usage_options.options_overrides.insert(
+ {CacheEntryRole::kBlockBasedTableReader,
+ {/*.charged = */ FLAGS_charge_table_reader
+ ? CacheEntryRoleOptions::Decision::kEnabled
+ : CacheEntryRoleOptions::Decision::kDisabled}});
+ block_based_options.cache_usage_options.options_overrides.insert(
+ {CacheEntryRole::kFileMetadata,
+ {/*.charged = */ FLAGS_charge_file_metadata
+ ? CacheEntryRoleOptions::Decision::kEnabled
+ : CacheEntryRoleOptions::Decision::kDisabled}});
+ block_based_options.cache_usage_options.options_overrides.insert(
+ {CacheEntryRole::kBlobCache,
+ {/*.charged = */ FLAGS_charge_blob_cache
+ ? CacheEntryRoleOptions::Decision::kEnabled
+ : CacheEntryRoleOptions::Decision::kDisabled}});
+ block_based_options.block_cache_compressed = compressed_cache_;
+ block_based_options.block_size = FLAGS_block_size;
+ block_based_options.block_restart_interval = FLAGS_block_restart_interval;
+ block_based_options.index_block_restart_interval =
+ FLAGS_index_block_restart_interval;
+ block_based_options.format_version =
+ static_cast<uint32_t>(FLAGS_format_version);
+ block_based_options.read_amp_bytes_per_bit = FLAGS_read_amp_bytes_per_bit;
+ block_based_options.enable_index_compression =
+ FLAGS_enable_index_compression;
+ block_based_options.block_align = FLAGS_block_align;
+ block_based_options.whole_key_filtering = FLAGS_whole_key_filtering;
+ block_based_options.max_auto_readahead_size =
+ FLAGS_max_auto_readahead_size;
+ block_based_options.initial_auto_readahead_size =
+ FLAGS_initial_auto_readahead_size;
+ block_based_options.num_file_reads_for_auto_readahead =
+ FLAGS_num_file_reads_for_auto_readahead;
+ BlockBasedTableOptions::PrepopulateBlockCache prepopulate_block_cache =
+ block_based_options.prepopulate_block_cache;
+ switch (FLAGS_prepopulate_block_cache) {
+ case 0:
+ prepopulate_block_cache =
+ BlockBasedTableOptions::PrepopulateBlockCache::kDisable;
+ break;
+ case 1:
+ prepopulate_block_cache =
+ BlockBasedTableOptions::PrepopulateBlockCache::kFlushOnly;
+ break;
+ default:
+ fprintf(stderr, "Unknown prepopulate block cache mode\n");
+ }
+ block_based_options.prepopulate_block_cache = prepopulate_block_cache;
+ if (FLAGS_use_data_block_hash_index) {
+ block_based_options.data_block_index_type =
+ ROCKSDB_NAMESPACE::BlockBasedTableOptions::kDataBlockBinaryAndHash;
+ } else {
+ block_based_options.data_block_index_type =
+ ROCKSDB_NAMESPACE::BlockBasedTableOptions::kDataBlockBinarySearch;
+ }
+ block_based_options.data_block_hash_table_util_ratio =
+ FLAGS_data_block_hash_table_util_ratio;
+ if (FLAGS_read_cache_path != "") {
+#ifndef ROCKSDB_LITE
+ Status rc_status;
+
+ // Read cache need to be provided with a the Logger, we will put all
+ // reac cache logs in the read cache path in a file named rc_LOG
+ rc_status = FLAGS_env->CreateDirIfMissing(FLAGS_read_cache_path);
+ std::shared_ptr<Logger> read_cache_logger;
+ if (rc_status.ok()) {
+ rc_status = FLAGS_env->NewLogger(FLAGS_read_cache_path + "/rc_LOG",
+ &read_cache_logger);
+ }
+
+ if (rc_status.ok()) {
+ PersistentCacheConfig rc_cfg(FLAGS_env, FLAGS_read_cache_path,
+ FLAGS_read_cache_size,
+ read_cache_logger);
+
+ rc_cfg.enable_direct_reads = FLAGS_read_cache_direct_read;
+ rc_cfg.enable_direct_writes = FLAGS_read_cache_direct_write;
+ rc_cfg.writer_qdepth = 4;
+ rc_cfg.writer_dispatch_size = 4 * 1024;
+
+ auto pcache = std::make_shared<BlockCacheTier>(rc_cfg);
+ block_based_options.persistent_cache = pcache;
+ rc_status = pcache->Open();
+ }
+
+ if (!rc_status.ok()) {
+ fprintf(stderr, "Error initializing read cache, %s\n",
+ rc_status.ToString().c_str());
+ exit(1);
+ }
+#else
+ fprintf(stderr, "Read cache is not supported in LITE\n");
+ exit(1);
+
+#endif
+ }
+
+ if (FLAGS_use_blob_cache) {
+ if (FLAGS_use_shared_block_and_blob_cache) {
+ options.blob_cache = cache_;
+ } else {
+ if (FLAGS_blob_cache_size > 0) {
+ LRUCacheOptions co;
+ co.capacity = FLAGS_blob_cache_size;
+ co.num_shard_bits = FLAGS_blob_cache_numshardbits;
+ co.memory_allocator = GetCacheAllocator();
+
+ options.blob_cache = NewLRUCache(co);
+ } else {
+ fprintf(
+ stderr,
+ "Unable to create a standalone blob cache if blob_cache_size "
+ "<= 0.\n");
+ exit(1);
+ }
+ }
+ switch (FLAGS_prepopulate_blob_cache) {
+ case 0:
+ options.prepopulate_blob_cache = PrepopulateBlobCache::kDisable;
+ break;
+ case 1:
+ options.prepopulate_blob_cache = PrepopulateBlobCache::kFlushOnly;
+ break;
+ default:
+ fprintf(stderr, "Unknown prepopulate blob cache mode\n");
+ exit(1);
+ }
+
+ fprintf(stdout,
+ "Integrated BlobDB: blob cache enabled"
+ ", block and blob caches shared: %d",
+ FLAGS_use_shared_block_and_blob_cache);
+ if (!FLAGS_use_shared_block_and_blob_cache) {
+ fprintf(stdout,
+ ", blob cache size %" PRIu64
+ ", blob cache num shard bits: %d",
+ FLAGS_blob_cache_size, FLAGS_blob_cache_numshardbits);
+ }
+ fprintf(stdout, ", blob cache prepopulated: %d\n",
+ FLAGS_prepopulate_blob_cache);
+ } else {
+ fprintf(stdout, "Integrated BlobDB: blob cache disabled\n");
+ }
+
+ options.table_factory.reset(
+ NewBlockBasedTableFactory(block_based_options));
+ }
+ if (FLAGS_max_bytes_for_level_multiplier_additional_v.size() > 0) {
+ if (FLAGS_max_bytes_for_level_multiplier_additional_v.size() !=
+ static_cast<unsigned int>(FLAGS_num_levels)) {
+ fprintf(stderr, "Insufficient number of fanouts specified %d\n",
+ static_cast<int>(
+ FLAGS_max_bytes_for_level_multiplier_additional_v.size()));
+ exit(1);
+ }
+ options.max_bytes_for_level_multiplier_additional =
+ FLAGS_max_bytes_for_level_multiplier_additional_v;
+ }
+ options.level0_stop_writes_trigger = FLAGS_level0_stop_writes_trigger;
+ options.level0_file_num_compaction_trigger =
+ FLAGS_level0_file_num_compaction_trigger;
+ options.level0_slowdown_writes_trigger =
+ FLAGS_level0_slowdown_writes_trigger;
+ options.compression = FLAGS_compression_type_e;
+ if (FLAGS_simulate_hybrid_fs_file != "") {
+ options.bottommost_temperature = Temperature::kWarm;
+ }
+ options.preclude_last_level_data_seconds =
+ FLAGS_preclude_last_level_data_seconds;
+ options.preserve_internal_time_seconds =
+ FLAGS_preserve_internal_time_seconds;
+ options.sample_for_compression = FLAGS_sample_for_compression;
+ options.WAL_ttl_seconds = FLAGS_wal_ttl_seconds;
+ options.WAL_size_limit_MB = FLAGS_wal_size_limit_MB;
+ options.max_total_wal_size = FLAGS_max_total_wal_size;
+
+ if (FLAGS_min_level_to_compress >= 0) {
+ assert(FLAGS_min_level_to_compress <= FLAGS_num_levels);
+ options.compression_per_level.resize(FLAGS_num_levels);
+ for (int i = 0; i < FLAGS_min_level_to_compress; i++) {
+ options.compression_per_level[i] = kNoCompression;
+ }
+ for (int i = FLAGS_min_level_to_compress; i < FLAGS_num_levels; i++) {
+ options.compression_per_level[i] = FLAGS_compression_type_e;
+ }
+ }
+ options.soft_pending_compaction_bytes_limit =
+ FLAGS_soft_pending_compaction_bytes_limit;
+ options.hard_pending_compaction_bytes_limit =
+ FLAGS_hard_pending_compaction_bytes_limit;
+ options.delayed_write_rate = FLAGS_delayed_write_rate;
+ options.allow_concurrent_memtable_write =
+ FLAGS_allow_concurrent_memtable_write;
+ options.experimental_mempurge_threshold =
+ FLAGS_experimental_mempurge_threshold;
+ options.inplace_update_support = FLAGS_inplace_update_support;
+ options.inplace_update_num_locks = FLAGS_inplace_update_num_locks;
+ options.enable_write_thread_adaptive_yield =
+ FLAGS_enable_write_thread_adaptive_yield;
+ options.enable_pipelined_write = FLAGS_enable_pipelined_write;
+ options.unordered_write = FLAGS_unordered_write;
+ options.write_thread_max_yield_usec = FLAGS_write_thread_max_yield_usec;
+ options.write_thread_slow_yield_usec = FLAGS_write_thread_slow_yield_usec;
+ options.table_cache_numshardbits = FLAGS_table_cache_numshardbits;
+ options.max_compaction_bytes = FLAGS_max_compaction_bytes;
+ options.disable_auto_compactions = FLAGS_disable_auto_compactions;
+ options.optimize_filters_for_hits = FLAGS_optimize_filters_for_hits;
+ options.paranoid_checks = FLAGS_paranoid_checks;
+ options.force_consistency_checks = FLAGS_force_consistency_checks;
+ options.check_flush_compaction_key_order =
+ FLAGS_check_flush_compaction_key_order;
+ options.periodic_compaction_seconds = FLAGS_periodic_compaction_seconds;
+ options.ttl = FLAGS_ttl_seconds;
+ // fill storage options
+ options.advise_random_on_open = FLAGS_advise_random_on_open;
+ options.access_hint_on_compaction_start = FLAGS_compaction_fadvice_e;
+ options.use_adaptive_mutex = FLAGS_use_adaptive_mutex;
+ options.bytes_per_sync = FLAGS_bytes_per_sync;
+ options.wal_bytes_per_sync = FLAGS_wal_bytes_per_sync;
+
+ // merge operator options
+ if (!FLAGS_merge_operator.empty()) {
+ s = MergeOperator::CreateFromString(config_options, FLAGS_merge_operator,
+ &options.merge_operator);
+ if (!s.ok()) {
+ fprintf(stderr, "invalid merge operator[%s]: %s\n",
+ FLAGS_merge_operator.c_str(), s.ToString().c_str());
+ exit(1);
+ }
+ }
+ options.max_successive_merges = FLAGS_max_successive_merges;
+ options.report_bg_io_stats = FLAGS_report_bg_io_stats;
+
+ // set universal style compaction configurations, if applicable
+ if (FLAGS_universal_size_ratio != 0) {
+ options.compaction_options_universal.size_ratio =
+ FLAGS_universal_size_ratio;
+ }
+ if (FLAGS_universal_min_merge_width != 0) {
+ options.compaction_options_universal.min_merge_width =
+ FLAGS_universal_min_merge_width;
+ }
+ if (FLAGS_universal_max_merge_width != 0) {
+ options.compaction_options_universal.max_merge_width =
+ FLAGS_universal_max_merge_width;
+ }
+ if (FLAGS_universal_max_size_amplification_percent != 0) {
+ options.compaction_options_universal.max_size_amplification_percent =
+ FLAGS_universal_max_size_amplification_percent;
+ }
+ if (FLAGS_universal_compression_size_percent != -1) {
+ options.compaction_options_universal.compression_size_percent =
+ FLAGS_universal_compression_size_percent;
+ }
+ options.compaction_options_universal.allow_trivial_move =
+ FLAGS_universal_allow_trivial_move;
+ options.compaction_options_universal.incremental =
+ FLAGS_universal_incremental;
+ if (FLAGS_thread_status_per_interval > 0) {
+ options.enable_thread_tracking = true;
+ }
+
+ if (FLAGS_user_timestamp_size > 0) {
+ if (FLAGS_user_timestamp_size != 8) {
+ fprintf(stderr, "Only 64 bits timestamps are supported.\n");
+ exit(1);
+ }
+ options.comparator = test::BytewiseComparatorWithU64TsWrapper();
+ }
+
+ options.allow_data_in_errors = FLAGS_allow_data_in_errors;
+ options.track_and_verify_wals_in_manifest =
+ FLAGS_track_and_verify_wals_in_manifest;
+
+ // Integrated BlobDB
+ options.enable_blob_files = FLAGS_enable_blob_files;
+ options.min_blob_size = FLAGS_min_blob_size;
+ options.blob_file_size = FLAGS_blob_file_size;
+ options.blob_compression_type =
+ StringToCompressionType(FLAGS_blob_compression_type.c_str());
+ options.enable_blob_garbage_collection =
+ FLAGS_enable_blob_garbage_collection;
+ options.blob_garbage_collection_age_cutoff =
+ FLAGS_blob_garbage_collection_age_cutoff;
+ options.blob_garbage_collection_force_threshold =
+ FLAGS_blob_garbage_collection_force_threshold;
+ options.blob_compaction_readahead_size =
+ FLAGS_blob_compaction_readahead_size;
+ options.blob_file_starting_level = FLAGS_blob_file_starting_level;
+
+#ifndef ROCKSDB_LITE
+ if (FLAGS_readonly && FLAGS_transaction_db) {
+ fprintf(stderr, "Cannot use readonly flag with transaction_db\n");
+ exit(1);
+ }
+ if (FLAGS_use_secondary_db &&
+ (FLAGS_transaction_db || FLAGS_optimistic_transaction_db)) {
+ fprintf(stderr, "Cannot use use_secondary_db flag with transaction_db\n");
+ exit(1);
+ }
+#endif // ROCKSDB_LITE
+ options.memtable_protection_bytes_per_key =
+ FLAGS_memtable_protection_bytes_per_key;
+ }
+
+ void InitializeOptionsGeneral(Options* opts) {
+ // Be careful about what is set here to avoid accidentally overwriting
+ // settings already configured by OPTIONS file. Only configure settings that
+ // are needed for the benchmark to run, settings for shared objects that
+ // were not configured already, settings that require dynamically invoking
+ // APIs, and settings for the benchmark itself.
+ Options& options = *opts;
+
+ // Always set these since they are harmless when not needed and prevent
+ // a guaranteed failure when they are needed.
+ options.create_missing_column_families = true;
+ options.create_if_missing = true;
+
+ if (options.statistics == nullptr) {
+ options.statistics = dbstats;
+ }
+
+ auto table_options =
+ options.table_factory->GetOptions<BlockBasedTableOptions>();
+ if (table_options != nullptr) {
+ if (FLAGS_cache_size > 0) {
+ // This violates this function's rules on when to set options. But we
+ // have to do it because the case of unconfigured block cache in OPTIONS
+ // file is indistinguishable (it is sanitized to 8MB by this point, not
+ // nullptr), and our regression tests assume this will be the shared
+ // block cache, even with OPTIONS file provided.
+ table_options->block_cache = cache_;
+ }
+ if (table_options->filter_policy == nullptr) {
+ if (FLAGS_bloom_bits < 0) {
+ table_options->filter_policy = BlockBasedTableOptions().filter_policy;
+ } else if (FLAGS_bloom_bits == 0) {
+ table_options->filter_policy.reset();
+ } else {
+ table_options->filter_policy.reset(
+ FLAGS_use_ribbon_filter ? NewRibbonFilterPolicy(FLAGS_bloom_bits)
+ : NewBloomFilterPolicy(FLAGS_bloom_bits));
+ }
+ }
+ }
+
+ if (options.row_cache == nullptr) {
+ if (FLAGS_row_cache_size) {
+ if (FLAGS_cache_numshardbits >= 1) {
+ options.row_cache =
+ NewLRUCache(FLAGS_row_cache_size, FLAGS_cache_numshardbits);
+ } else {
+ options.row_cache = NewLRUCache(FLAGS_row_cache_size);
+ }
+ }
+ }
+
+ if (options.env == Env::Default()) {
+ options.env = FLAGS_env;
+ }
+ if (FLAGS_enable_io_prio) {
+ options.env->LowerThreadPoolIOPriority(Env::LOW);
+ options.env->LowerThreadPoolIOPriority(Env::HIGH);
+ }
+ if (FLAGS_enable_cpu_prio) {
+ options.env->LowerThreadPoolCPUPriority(Env::LOW);
+ options.env->LowerThreadPoolCPUPriority(Env::HIGH);
+ }
+
+ if (FLAGS_sine_write_rate) {
+ FLAGS_benchmark_write_rate_limit = static_cast<uint64_t>(SineRate(0));
+ }
+
+ if (options.rate_limiter == nullptr) {
+ if (FLAGS_rate_limiter_bytes_per_sec > 0) {
+ options.rate_limiter.reset(NewGenericRateLimiter(
+ FLAGS_rate_limiter_bytes_per_sec,
+ FLAGS_rate_limiter_refill_period_us, 10 /* fairness */,
+ // TODO: replace this with a more general FLAG for deciding
+ // RateLimiter::Mode as now we also rate-limit foreground reads e.g,
+ // Get()/MultiGet()
+ FLAGS_rate_limit_bg_reads ? RateLimiter::Mode::kReadsOnly
+ : RateLimiter::Mode::kWritesOnly,
+ FLAGS_rate_limiter_auto_tuned));
+ }
+ }
+
+ options.listeners.emplace_back(listener_);
+
+ if (options.file_checksum_gen_factory == nullptr) {
+ if (FLAGS_file_checksum) {
+ options.file_checksum_gen_factory.reset(
+ new FileChecksumGenCrc32cFactory());
+ }
+ }
+
+ if (FLAGS_num_multi_db <= 1) {
+ OpenDb(options, FLAGS_db, &db_);
+ } else {
+ multi_dbs_.clear();
+ multi_dbs_.resize(FLAGS_num_multi_db);
+ auto wal_dir = options.wal_dir;
+ for (int i = 0; i < FLAGS_num_multi_db; i++) {
+ if (!wal_dir.empty()) {
+ options.wal_dir = GetPathForMultiple(wal_dir, i);
+ }
+ OpenDb(options, GetPathForMultiple(FLAGS_db, i), &multi_dbs_[i]);
+ }
+ options.wal_dir = wal_dir;
+ }
+
+ // KeepFilter is a noop filter, this can be used to test compaction filter
+ if (options.compaction_filter == nullptr) {
+ if (FLAGS_use_keep_filter) {
+ options.compaction_filter = new KeepFilter();
+ fprintf(stdout, "A noop compaction filter is used\n");
+ }
+ }
+
+ if (FLAGS_use_existing_keys) {
+ // Only work on single database
+ assert(db_.db != nullptr);
+ ReadOptions read_opts; // before read_options_ initialized
+ read_opts.total_order_seek = true;
+ Iterator* iter = db_.db->NewIterator(read_opts);
+ for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
+ keys_.emplace_back(iter->key().ToString());
+ }
+ delete iter;
+ FLAGS_num = keys_.size();
+ }
+ }
+
+ void Open(Options* opts) {
+ if (!InitializeOptionsFromFile(opts)) {
+ InitializeOptionsFromFlags(opts);
+ }
+
+ InitializeOptionsGeneral(opts);
+ }
+
+ void OpenDb(Options options, const std::string& db_name,
+ DBWithColumnFamilies* db) {
+ uint64_t open_start = FLAGS_report_open_timing ? FLAGS_env->NowNanos() : 0;
+ Status s;
+ // Open with column families if necessary.
+ if (FLAGS_num_column_families > 1) {
+ size_t num_hot = FLAGS_num_column_families;
+ if (FLAGS_num_hot_column_families > 0 &&
+ FLAGS_num_hot_column_families < FLAGS_num_column_families) {
+ num_hot = FLAGS_num_hot_column_families;
+ } else {
+ FLAGS_num_hot_column_families = FLAGS_num_column_families;
+ }
+ std::vector<ColumnFamilyDescriptor> column_families;
+ for (size_t i = 0; i < num_hot; i++) {
+ column_families.push_back(ColumnFamilyDescriptor(
+ ColumnFamilyName(i), ColumnFamilyOptions(options)));
+ }
+ std::vector<int> cfh_idx_to_prob;
+ if (!FLAGS_column_family_distribution.empty()) {
+ std::stringstream cf_prob_stream(FLAGS_column_family_distribution);
+ std::string cf_prob;
+ int sum = 0;
+ while (std::getline(cf_prob_stream, cf_prob, ',')) {
+ cfh_idx_to_prob.push_back(std::stoi(cf_prob));
+ sum += cfh_idx_to_prob.back();
+ }
+ if (sum != 100) {
+ fprintf(stderr, "column_family_distribution items must sum to 100\n");
+ exit(1);
+ }
+ if (cfh_idx_to_prob.size() != num_hot) {
+ fprintf(stderr,
+ "got %" ROCKSDB_PRIszt
+ " column_family_distribution items; expected "
+ "%" ROCKSDB_PRIszt "\n",
+ cfh_idx_to_prob.size(), num_hot);
+ exit(1);
+ }
+ }
+#ifndef ROCKSDB_LITE
+ if (FLAGS_readonly) {
+ s = DB::OpenForReadOnly(options, db_name, column_families, &db->cfh,
+ &db->db);
+ } else if (FLAGS_optimistic_transaction_db) {
+ s = OptimisticTransactionDB::Open(options, db_name, column_families,
+ &db->cfh, &db->opt_txn_db);
+ if (s.ok()) {
+ db->db = db->opt_txn_db->GetBaseDB();
+ }
+ } else if (FLAGS_transaction_db) {
+ TransactionDB* ptr;
+ TransactionDBOptions txn_db_options;
+ if (options.unordered_write) {
+ options.two_write_queues = true;
+ txn_db_options.skip_concurrency_control = true;
+ txn_db_options.write_policy = WRITE_PREPARED;
+ }
+ s = TransactionDB::Open(options, txn_db_options, db_name,
+ column_families, &db->cfh, &ptr);
+ if (s.ok()) {
+ db->db = ptr;
+ }
+ } else {
+ s = DB::Open(options, db_name, column_families, &db->cfh, &db->db);
+ }
+#else
+ s = DB::Open(options, db_name, column_families, &db->cfh, &db->db);
+#endif // ROCKSDB_LITE
+ db->cfh.resize(FLAGS_num_column_families);
+ db->num_created = num_hot;
+ db->num_hot = num_hot;
+ db->cfh_idx_to_prob = std::move(cfh_idx_to_prob);
+#ifndef ROCKSDB_LITE
+ } else if (FLAGS_readonly) {
+ s = DB::OpenForReadOnly(options, db_name, &db->db);
+ } else if (FLAGS_optimistic_transaction_db) {
+ s = OptimisticTransactionDB::Open(options, db_name, &db->opt_txn_db);
+ if (s.ok()) {
+ db->db = db->opt_txn_db->GetBaseDB();
+ }
+ } else if (FLAGS_transaction_db) {
+ TransactionDB* ptr = nullptr;
+ TransactionDBOptions txn_db_options;
+ if (options.unordered_write) {
+ options.two_write_queues = true;
+ txn_db_options.skip_concurrency_control = true;
+ txn_db_options.write_policy = WRITE_PREPARED;
+ }
+ s = CreateLoggerFromOptions(db_name, options, &options.info_log);
+ if (s.ok()) {
+ s = TransactionDB::Open(options, txn_db_options, db_name, &ptr);
+ }
+ if (s.ok()) {
+ db->db = ptr;
+ }
+ } else if (FLAGS_use_blob_db) {
+ // Stacked BlobDB
+ blob_db::BlobDBOptions blob_db_options;
+ blob_db_options.enable_garbage_collection = FLAGS_blob_db_enable_gc;
+ blob_db_options.garbage_collection_cutoff = FLAGS_blob_db_gc_cutoff;
+ blob_db_options.is_fifo = FLAGS_blob_db_is_fifo;
+ blob_db_options.max_db_size = FLAGS_blob_db_max_db_size;
+ blob_db_options.ttl_range_secs = FLAGS_blob_db_ttl_range_secs;
+ blob_db_options.min_blob_size = FLAGS_blob_db_min_blob_size;
+ blob_db_options.bytes_per_sync = FLAGS_blob_db_bytes_per_sync;
+ blob_db_options.blob_file_size = FLAGS_blob_db_file_size;
+ blob_db_options.compression = FLAGS_blob_db_compression_type_e;
+ blob_db::BlobDB* ptr = nullptr;
+ s = blob_db::BlobDB::Open(options, blob_db_options, db_name, &ptr);
+ if (s.ok()) {
+ db->db = ptr;
+ }
+ } else if (FLAGS_use_secondary_db) {
+ if (FLAGS_secondary_path.empty()) {
+ std::string default_secondary_path;
+ FLAGS_env->GetTestDirectory(&default_secondary_path);
+ default_secondary_path += "/dbbench_secondary";
+ FLAGS_secondary_path = default_secondary_path;
+ }
+ s = DB::OpenAsSecondary(options, db_name, FLAGS_secondary_path, &db->db);
+ if (s.ok() && FLAGS_secondary_update_interval > 0) {
+ secondary_update_thread_.reset(new port::Thread(
+ [this](int interval, DBWithColumnFamilies* _db) {
+ while (0 == secondary_update_stopped_.load(
+ std::memory_order_relaxed)) {
+ Status secondary_update_status =
+ _db->db->TryCatchUpWithPrimary();
+ if (!secondary_update_status.ok()) {
+ fprintf(stderr, "Failed to catch up with primary: %s\n",
+ secondary_update_status.ToString().c_str());
+ break;
+ }
+ ++secondary_db_updates_;
+ FLAGS_env->SleepForMicroseconds(interval * 1000000);
+ }
+ },
+ FLAGS_secondary_update_interval, db));
+ }
+#endif // ROCKSDB_LITE
+ } else {
+ s = DB::Open(options, db_name, &db->db);
+ }
+ if (FLAGS_report_open_timing) {
+ std::cout << "OpenDb: "
+ << (FLAGS_env->NowNanos() - open_start) / 1000000.0
+ << " milliseconds\n";
+ }
+ if (!s.ok()) {
+ fprintf(stderr, "open error: %s\n", s.ToString().c_str());
+ exit(1);
+ }
+ }
+
+ enum WriteMode { RANDOM, SEQUENTIAL, UNIQUE_RANDOM };
+
+ void WriteSeqDeterministic(ThreadState* thread) {
+ DoDeterministicCompact(thread, open_options_.compaction_style, SEQUENTIAL);
+ }
+
+ void WriteUniqueRandomDeterministic(ThreadState* thread) {
+ DoDeterministicCompact(thread, open_options_.compaction_style,
+ UNIQUE_RANDOM);
+ }
+
+ void WriteSeq(ThreadState* thread) { DoWrite(thread, SEQUENTIAL); }
+
+ void WriteRandom(ThreadState* thread) { DoWrite(thread, RANDOM); }
+
+ void WriteUniqueRandom(ThreadState* thread) {
+ DoWrite(thread, UNIQUE_RANDOM);
+ }
+
+ class KeyGenerator {
+ public:
+ KeyGenerator(Random64* rand, WriteMode mode, uint64_t num,
+ uint64_t /*num_per_set*/ = 64 * 1024)
+ : rand_(rand), mode_(mode), num_(num), next_(0) {
+ if (mode_ == UNIQUE_RANDOM) {
+ // NOTE: if memory consumption of this approach becomes a concern,
+ // we can either break it into pieces and only random shuffle a section
+ // each time. Alternatively, use a bit map implementation
+ // (https://reviews.facebook.net/differential/diff/54627/)
+ values_.resize(num_);
+ for (uint64_t i = 0; i < num_; ++i) {
+ values_[i] = i;
+ }
+ RandomShuffle(values_.begin(), values_.end(),
+ static_cast<uint32_t>(seed_base));
+ }
+ }
+
+ uint64_t Next() {
+ switch (mode_) {
+ case SEQUENTIAL:
+ return next_++;
+ case RANDOM:
+ return rand_->Next() % num_;
+ case UNIQUE_RANDOM:
+ assert(next_ < num_);
+ return values_[next_++];
+ }
+ assert(false);
+ return std::numeric_limits<uint64_t>::max();
+ }
+
+ // Only available for UNIQUE_RANDOM mode.
+ uint64_t Fetch(uint64_t index) {
+ assert(mode_ == UNIQUE_RANDOM);
+ assert(index < values_.size());
+ return values_[index];
+ }
+
+ private:
+ Random64* rand_;
+ WriteMode mode_;
+ const uint64_t num_;
+ uint64_t next_;
+ std::vector<uint64_t> values_;
+ };
+
+ DB* SelectDB(ThreadState* thread) { return SelectDBWithCfh(thread)->db; }
+
+ DBWithColumnFamilies* SelectDBWithCfh(ThreadState* thread) {
+ return SelectDBWithCfh(thread->rand.Next());
+ }
+
+ DBWithColumnFamilies* SelectDBWithCfh(uint64_t rand_int) {
+ if (db_.db != nullptr) {
+ return &db_;
+ } else {
+ return &multi_dbs_[rand_int % multi_dbs_.size()];
+ }
+ }
+
+ double SineRate(double x) {
+ return FLAGS_sine_a * sin((FLAGS_sine_b * x) + FLAGS_sine_c) + FLAGS_sine_d;
+ }
+
+ void DoWrite(ThreadState* thread, WriteMode write_mode) {
+ const int test_duration = write_mode == RANDOM ? FLAGS_duration : 0;
+ const int64_t num_ops = writes_ == 0 ? num_ : writes_;
+
+ size_t num_key_gens = 1;
+ if (db_.db == nullptr) {
+ num_key_gens = multi_dbs_.size();
+ }
+ std::vector<std::unique_ptr<KeyGenerator>> key_gens(num_key_gens);
+ int64_t max_ops = num_ops * num_key_gens;
+ int64_t ops_per_stage = max_ops;
+ if (FLAGS_num_column_families > 1 && FLAGS_num_hot_column_families > 0) {
+ ops_per_stage = (max_ops - 1) / (FLAGS_num_column_families /
+ FLAGS_num_hot_column_families) +
+ 1;
+ }
+
+ Duration duration(test_duration, max_ops, ops_per_stage);
+ const uint64_t num_per_key_gen = num_ + max_num_range_tombstones_;
+ for (size_t i = 0; i < num_key_gens; i++) {
+ key_gens[i].reset(new KeyGenerator(&(thread->rand), write_mode,
+ num_per_key_gen, ops_per_stage));
+ }
+
+ if (num_ != FLAGS_num) {
+ char msg[100];
+ snprintf(msg, sizeof(msg), "(%" PRIu64 " ops)", num_);
+ thread->stats.AddMessage(msg);
+ }
+
+ RandomGenerator gen;
+ WriteBatch batch(/*reserved_bytes=*/0, /*max_bytes=*/0,
+ FLAGS_write_batch_protection_bytes_per_key,
+ user_timestamp_size_);
+ Status s;
+ int64_t bytes = 0;
+
+ std::unique_ptr<const char[]> key_guard;
+ Slice key = AllocateKey(&key_guard);
+ std::unique_ptr<const char[]> begin_key_guard;
+ Slice begin_key = AllocateKey(&begin_key_guard);
+ std::unique_ptr<const char[]> end_key_guard;
+ Slice end_key = AllocateKey(&end_key_guard);
+ double p = 0.0;
+ uint64_t num_overwrites = 0, num_unique_keys = 0, num_selective_deletes = 0;
+ // If user set overwrite_probability flag,
+ // check if value is in [0.0,1.0].
+ if (FLAGS_overwrite_probability > 0.0) {
+ p = FLAGS_overwrite_probability > 1.0 ? 1.0 : FLAGS_overwrite_probability;
+ // If overwrite set by user, and UNIQUE_RANDOM mode on,
+ // the overwrite_window_size must be > 0.
+ if (write_mode == UNIQUE_RANDOM && FLAGS_overwrite_window_size == 0) {
+ fprintf(stderr,
+ "Overwrite_window_size must be strictly greater than 0.\n");
+ ErrorExit();
+ }
+ }
+
+ // Default_random_engine provides slightly
+ // improved throughput over mt19937.
+ std::default_random_engine overwrite_gen{
+ static_cast<unsigned int>(seed_base)};
+ std::bernoulli_distribution overwrite_decider(p);
+
+ // Inserted key window is filled with the last N
+ // keys previously inserted into the DB (with
+ // N=FLAGS_overwrite_window_size).
+ // We use a deque struct because:
+ // - random access is O(1)
+ // - insertion/removal at beginning/end is also O(1).
+ std::deque<int64_t> inserted_key_window;
+ Random64 reservoir_id_gen(seed_base);
+
+ // --- Variables used in disposable/persistent keys simulation:
+ // The following variables are used when
+ // disposable_entries_batch_size is >0. We simualte a workload
+ // where the following sequence is repeated multiple times:
+ // "A set of keys S1 is inserted ('disposable entries'), then after
+ // some delay another set of keys S2 is inserted ('persistent entries')
+ // and the first set of keys S1 is deleted. S2 artificially represents
+ // the insertion of hypothetical results from some undefined computation
+ // done on the first set of keys S1. The next sequence can start as soon
+ // as the last disposable entry in the set S1 of this sequence is
+ // inserted, if the delay is non negligible"
+ bool skip_for_loop = false, is_disposable_entry = true;
+ std::vector<uint64_t> disposable_entries_index(num_key_gens, 0);
+ std::vector<uint64_t> persistent_ent_and_del_index(num_key_gens, 0);
+ const uint64_t kNumDispAndPersEntries =
+ FLAGS_disposable_entries_batch_size +
+ FLAGS_persistent_entries_batch_size;
+ if (kNumDispAndPersEntries > 0) {
+ if ((write_mode != UNIQUE_RANDOM) || (writes_per_range_tombstone_ > 0) ||
+ (p > 0.0)) {
+ fprintf(
+ stderr,
+ "Disposable/persistent deletes are not compatible with overwrites "
+ "and DeleteRanges; and are only supported in filluniquerandom.\n");
+ ErrorExit();
+ }
+ if (FLAGS_disposable_entries_value_size < 0 ||
+ FLAGS_persistent_entries_value_size < 0) {
+ fprintf(
+ stderr,
+ "disposable_entries_value_size and persistent_entries_value_size"
+ "have to be positive.\n");
+ ErrorExit();
+ }
+ }
+ Random rnd_disposable_entry(static_cast<uint32_t>(seed_base));
+ std::string random_value;
+ // Queue that stores scheduled timestamp of disposable entries deletes,
+ // along with starting index of disposable entry keys to delete.
+ std::vector<std::queue<std::pair<uint64_t, uint64_t>>> disposable_entries_q(
+ num_key_gens);
+ // --- End of variables used in disposable/persistent keys simulation.
+
+ std::vector<std::unique_ptr<const char[]>> expanded_key_guards;
+ std::vector<Slice> expanded_keys;
+ if (FLAGS_expand_range_tombstones) {
+ expanded_key_guards.resize(range_tombstone_width_);
+ for (auto& expanded_key_guard : expanded_key_guards) {
+ expanded_keys.emplace_back(AllocateKey(&expanded_key_guard));
+ }
+ }
+
+ std::unique_ptr<char[]> ts_guard;
+ if (user_timestamp_size_ > 0) {
+ ts_guard.reset(new char[user_timestamp_size_]);
+ }
+
+ int64_t stage = 0;
+ int64_t num_written = 0;
+ int64_t next_seq_db_at = num_ops;
+ size_t id = 0;
+ int64_t num_range_deletions = 0;
+
+ while ((num_per_key_gen != 0) && !duration.Done(entries_per_batch_)) {
+ if (duration.GetStage() != stage) {
+ stage = duration.GetStage();
+ if (db_.db != nullptr) {
+ db_.CreateNewCf(open_options_, stage);
+ } else {
+ for (auto& db : multi_dbs_) {
+ db.CreateNewCf(open_options_, stage);
+ }
+ }
+ }
+
+ if (write_mode != SEQUENTIAL) {
+ id = thread->rand.Next() % num_key_gens;
+ } else {
+ // When doing a sequential load with multiple databases, load them in
+ // order rather than all at the same time to avoid:
+ // 1) long delays between flushing memtables
+ // 2) flushing memtables for all of them at the same point in time
+ // 3) not putting the same number of keys in each database
+ if (num_written >= next_seq_db_at) {
+ next_seq_db_at += num_ops;
+ id++;
+ if (id >= num_key_gens) {
+ fprintf(stderr, "Logic error. Filled all databases\n");
+ ErrorExit();
+ }
+ }
+ }
+ DBWithColumnFamilies* db_with_cfh = SelectDBWithCfh(id);
+
+ batch.Clear();
+ int64_t batch_bytes = 0;
+
+ for (int64_t j = 0; j < entries_per_batch_; j++) {
+ int64_t rand_num = 0;
+ if ((write_mode == UNIQUE_RANDOM) && (p > 0.0)) {
+ if ((inserted_key_window.size() > 0) &&
+ overwrite_decider(overwrite_gen)) {
+ num_overwrites++;
+ rand_num = inserted_key_window[reservoir_id_gen.Next() %
+ inserted_key_window.size()];
+ } else {
+ num_unique_keys++;
+ rand_num = key_gens[id]->Next();
+ if (inserted_key_window.size() < FLAGS_overwrite_window_size) {
+ inserted_key_window.push_back(rand_num);
+ } else {
+ inserted_key_window.pop_front();
+ inserted_key_window.push_back(rand_num);
+ }
+ }
+ } else if (kNumDispAndPersEntries > 0) {
+ // Check if queue is non-empty and if we need to insert
+ // 'persistent' KV entries (KV entries that are never deleted)
+ // and delete disposable entries previously inserted.
+ if (!disposable_entries_q[id].empty() &&
+ (disposable_entries_q[id].front().first <
+ FLAGS_env->NowMicros())) {
+ // If we need to perform a "merge op" pattern,
+ // we first write all the persistent KV entries not targeted
+ // by deletes, and then we write the disposable entries deletes.
+ if (persistent_ent_and_del_index[id] <
+ FLAGS_persistent_entries_batch_size) {
+ // Generate key to insert.
+ rand_num =
+ key_gens[id]->Fetch(disposable_entries_q[id].front().second +
+ FLAGS_disposable_entries_batch_size +
+ persistent_ent_and_del_index[id]);
+ persistent_ent_and_del_index[id]++;
+ is_disposable_entry = false;
+ skip_for_loop = false;
+ } else if (persistent_ent_and_del_index[id] <
+ kNumDispAndPersEntries) {
+ // Find key of the entry to delete.
+ rand_num =
+ key_gens[id]->Fetch(disposable_entries_q[id].front().second +
+ (persistent_ent_and_del_index[id] -
+ FLAGS_persistent_entries_batch_size));
+ persistent_ent_and_del_index[id]++;
+ GenerateKeyFromInt(rand_num, FLAGS_num, &key);
+ // For the delete operation, everything happens here and we
+ // skip the rest of the for-loop, which is designed for
+ // inserts.
+ if (FLAGS_num_column_families <= 1) {
+ batch.Delete(key);
+ } else {
+ // We use same rand_num as seed for key and column family so
+ // that we can deterministically find the cfh corresponding to a
+ // particular key while reading the key.
+ batch.Delete(db_with_cfh->GetCfh(rand_num), key);
+ }
+ // A delete only includes Key+Timestamp (no value).
+ batch_bytes += key_size_ + user_timestamp_size_;
+ bytes += key_size_ + user_timestamp_size_;
+ num_selective_deletes++;
+ // Skip rest of the for-loop (j=0, j<entries_per_batch_,j++).
+ skip_for_loop = true;
+ } else {
+ assert(false); // should never reach this point.
+ }
+ // If disposable_entries_q needs to be updated (ie: when a selective
+ // insert+delete was successfully completed, pop the job out of the
+ // queue).
+ if (!disposable_entries_q[id].empty() &&
+ (disposable_entries_q[id].front().first <
+ FLAGS_env->NowMicros()) &&
+ persistent_ent_and_del_index[id] == kNumDispAndPersEntries) {
+ disposable_entries_q[id].pop();
+ persistent_ent_and_del_index[id] = 0;
+ }
+
+ // If we are deleting disposable entries, skip the rest of the
+ // for-loop since there is no key-value inserts at this moment in
+ // time.
+ if (skip_for_loop) {
+ continue;
+ }
+
+ }
+ // If no job is in the queue, then we keep inserting disposable KV
+ // entries that will be deleted later by a series of deletes.
+ else {
+ rand_num = key_gens[id]->Fetch(disposable_entries_index[id]);
+ disposable_entries_index[id]++;
+ is_disposable_entry = true;
+ if ((disposable_entries_index[id] %
+ FLAGS_disposable_entries_batch_size) == 0) {
+ // Skip the persistent KV entries inserts for now
+ disposable_entries_index[id] +=
+ FLAGS_persistent_entries_batch_size;
+ }
+ }
+ } else {
+ rand_num = key_gens[id]->Next();
+ }
+ GenerateKeyFromInt(rand_num, FLAGS_num, &key);
+ Slice val;
+ if (kNumDispAndPersEntries > 0) {
+ random_value = rnd_disposable_entry.RandomString(
+ is_disposable_entry ? FLAGS_disposable_entries_value_size
+ : FLAGS_persistent_entries_value_size);
+ val = Slice(random_value);
+ num_unique_keys++;
+ } else {
+ val = gen.Generate();
+ }
+ if (use_blob_db_) {
+#ifndef ROCKSDB_LITE
+ // Stacked BlobDB
+ blob_db::BlobDB* blobdb =
+ static_cast<blob_db::BlobDB*>(db_with_cfh->db);
+ if (FLAGS_blob_db_max_ttl_range > 0) {
+ int ttl = rand() % FLAGS_blob_db_max_ttl_range;
+ s = blobdb->PutWithTTL(write_options_, key, val, ttl);
+ } else {
+ s = blobdb->Put(write_options_, key, val);
+ }
+#endif // ROCKSDB_LITE
+ } else if (FLAGS_num_column_families <= 1) {
+ batch.Put(key, val);
+ } else {
+ // We use same rand_num as seed for key and column family so that we
+ // can deterministically find the cfh corresponding to a particular
+ // key while reading the key.
+ batch.Put(db_with_cfh->GetCfh(rand_num), key, val);
+ }
+ batch_bytes += val.size() + key_size_ + user_timestamp_size_;
+ bytes += val.size() + key_size_ + user_timestamp_size_;
+ ++num_written;
+
+ // If all disposable entries have been inserted, then we need to
+ // add in the job queue a call for 'persistent entry insertions +
+ // disposable entry deletions'.
+ if (kNumDispAndPersEntries > 0 && is_disposable_entry &&
+ ((disposable_entries_index[id] % kNumDispAndPersEntries) == 0)) {
+ // Queue contains [timestamp, starting_idx],
+ // timestamp = current_time + delay (minimum aboslute time when to
+ // start inserting the selective deletes) starting_idx = index in the
+ // keygen of the rand_num to generate the key of the first KV entry to
+ // delete (= key of the first selective delete).
+ disposable_entries_q[id].push(std::make_pair(
+ FLAGS_env->NowMicros() +
+ FLAGS_disposable_entries_delete_delay /* timestamp */,
+ disposable_entries_index[id] - kNumDispAndPersEntries
+ /*starting idx*/));
+ }
+ if (writes_per_range_tombstone_ > 0 &&
+ num_written > writes_before_delete_range_ &&
+ (num_written - writes_before_delete_range_) /
+ writes_per_range_tombstone_ <=
+ max_num_range_tombstones_ &&
+ (num_written - writes_before_delete_range_) %
+ writes_per_range_tombstone_ ==
+ 0) {
+ num_range_deletions++;
+ int64_t begin_num = key_gens[id]->Next();
+ if (FLAGS_expand_range_tombstones) {
+ for (int64_t offset = 0; offset < range_tombstone_width_;
+ ++offset) {
+ GenerateKeyFromInt(begin_num + offset, FLAGS_num,
+ &expanded_keys[offset]);
+ if (use_blob_db_) {
+#ifndef ROCKSDB_LITE
+ // Stacked BlobDB
+ s = db_with_cfh->db->Delete(write_options_,
+ expanded_keys[offset]);
+#endif // ROCKSDB_LITE
+ } else if (FLAGS_num_column_families <= 1) {
+ batch.Delete(expanded_keys[offset]);
+ } else {
+ batch.Delete(db_with_cfh->GetCfh(rand_num),
+ expanded_keys[offset]);
+ }
+ }
+ } else {
+ GenerateKeyFromInt(begin_num, FLAGS_num, &begin_key);
+ GenerateKeyFromInt(begin_num + range_tombstone_width_, FLAGS_num,
+ &end_key);
+ if (use_blob_db_) {
+#ifndef ROCKSDB_LITE
+ // Stacked BlobDB
+ s = db_with_cfh->db->DeleteRange(
+ write_options_, db_with_cfh->db->DefaultColumnFamily(),
+ begin_key, end_key);
+#endif // ROCKSDB_LITE
+ } else if (FLAGS_num_column_families <= 1) {
+ batch.DeleteRange(begin_key, end_key);
+ } else {
+ batch.DeleteRange(db_with_cfh->GetCfh(rand_num), begin_key,
+ end_key);
+ }
+ }
+ }
+ }
+ if (thread->shared->write_rate_limiter.get() != nullptr) {
+ thread->shared->write_rate_limiter->Request(
+ batch_bytes, Env::IO_HIGH, nullptr /* stats */,
+ RateLimiter::OpType::kWrite);
+ // Set time at which last op finished to Now() to hide latency and
+ // sleep from rate limiter. Also, do the check once per batch, not
+ // once per write.
+ thread->stats.ResetLastOpTime();
+ }
+ if (user_timestamp_size_ > 0) {
+ Slice user_ts = mock_app_clock_->Allocate(ts_guard.get());
+ s = batch.UpdateTimestamps(
+ user_ts, [this](uint32_t) { return user_timestamp_size_; });
+ if (!s.ok()) {
+ fprintf(stderr, "assign timestamp to write batch: %s\n",
+ s.ToString().c_str());
+ ErrorExit();
+ }
+ }
+ if (!use_blob_db_) {
+ // Not stacked BlobDB
+ s = db_with_cfh->db->Write(write_options_, &batch);
+ }
+ thread->stats.FinishedOps(db_with_cfh, db_with_cfh->db,
+ entries_per_batch_, kWrite);
+ if (FLAGS_sine_write_rate) {
+ uint64_t now = FLAGS_env->NowMicros();
+
+ uint64_t usecs_since_last;
+ if (now > thread->stats.GetSineInterval()) {
+ usecs_since_last = now - thread->stats.GetSineInterval();
+ } else {
+ usecs_since_last = 0;
+ }
+
+ if (usecs_since_last >
+ (FLAGS_sine_write_rate_interval_milliseconds * uint64_t{1000})) {
+ double usecs_since_start =
+ static_cast<double>(now - thread->stats.GetStart());
+ thread->stats.ResetSineInterval();
+ uint64_t write_rate =
+ static_cast<uint64_t>(SineRate(usecs_since_start / 1000000.0));
+ thread->shared->write_rate_limiter.reset(
+ NewGenericRateLimiter(write_rate));
+ }
+ }
+ if (!s.ok()) {
+ s = listener_->WaitForRecovery(600000000) ? Status::OK() : s;
+ }
+
+ if (!s.ok()) {
+ fprintf(stderr, "put error: %s\n", s.ToString().c_str());
+ ErrorExit();
+ }
+ }
+ if ((write_mode == UNIQUE_RANDOM) && (p > 0.0)) {
+ fprintf(stdout,
+ "Number of unique keys inserted: %" PRIu64
+ ".\nNumber of overwrites: %" PRIu64 "\n",
+ num_unique_keys, num_overwrites);
+ } else if (kNumDispAndPersEntries > 0) {
+ fprintf(stdout,
+ "Number of unique keys inserted (disposable+persistent): %" PRIu64
+ ".\nNumber of 'disposable entry delete': %" PRIu64 "\n",
+ num_written, num_selective_deletes);
+ }
+ if (num_range_deletions > 0) {
+ std::cout << "Number of range deletions: " << num_range_deletions
+ << std::endl;
+ }
+ thread->stats.AddBytes(bytes);
+ }
+
+ Status DoDeterministicCompact(ThreadState* thread,
+ CompactionStyle compaction_style,
+ WriteMode write_mode) {
+#ifndef ROCKSDB_LITE
+ ColumnFamilyMetaData meta;
+ std::vector<DB*> db_list;
+ if (db_.db != nullptr) {
+ db_list.push_back(db_.db);
+ } else {
+ for (auto& db : multi_dbs_) {
+ db_list.push_back(db.db);
+ }
+ }
+ std::vector<Options> options_list;
+ for (auto db : db_list) {
+ options_list.push_back(db->GetOptions());
+ if (compaction_style != kCompactionStyleFIFO) {
+ db->SetOptions({{"disable_auto_compactions", "1"},
+ {"level0_slowdown_writes_trigger", "400000000"},
+ {"level0_stop_writes_trigger", "400000000"}});
+ } else {
+ db->SetOptions({{"disable_auto_compactions", "1"}});
+ }
+ }
+
+ assert(!db_list.empty());
+ auto num_db = db_list.size();
+ size_t num_levels = static_cast<size_t>(open_options_.num_levels);
+ size_t output_level = open_options_.num_levels - 1;
+ std::vector<std::vector<std::vector<SstFileMetaData>>> sorted_runs(num_db);
+ std::vector<size_t> num_files_at_level0(num_db, 0);
+ if (compaction_style == kCompactionStyleLevel) {
+ if (num_levels == 0) {
+ return Status::InvalidArgument("num_levels should be larger than 1");
+ }
+ bool should_stop = false;
+ while (!should_stop) {
+ if (sorted_runs[0].empty()) {
+ DoWrite(thread, write_mode);
+ } else {
+ DoWrite(thread, UNIQUE_RANDOM);
+ }
+ for (size_t i = 0; i < num_db; i++) {
+ auto db = db_list[i];
+ db->Flush(FlushOptions());
+ db->GetColumnFamilyMetaData(&meta);
+ if (num_files_at_level0[i] == meta.levels[0].files.size() ||
+ writes_ == 0) {
+ should_stop = true;
+ continue;
+ }
+ sorted_runs[i].emplace_back(
+ meta.levels[0].files.begin(),
+ meta.levels[0].files.end() - num_files_at_level0[i]);
+ num_files_at_level0[i] = meta.levels[0].files.size();
+ if (sorted_runs[i].back().size() == 1) {
+ should_stop = true;
+ continue;
+ }
+ if (sorted_runs[i].size() == output_level) {
+ auto& L1 = sorted_runs[i].back();
+ L1.erase(L1.begin(), L1.begin() + L1.size() / 3);
+ should_stop = true;
+ continue;
+ }
+ }
+ writes_ /=
+ static_cast<int64_t>(open_options_.max_bytes_for_level_multiplier);
+ }
+ for (size_t i = 0; i < num_db; i++) {
+ if (sorted_runs[i].size() < num_levels - 1) {
+ fprintf(stderr, "n is too small to fill %" ROCKSDB_PRIszt " levels\n",
+ num_levels);
+ exit(1);
+ }
+ }
+ for (size_t i = 0; i < num_db; i++) {
+ auto db = db_list[i];
+ auto compactionOptions = CompactionOptions();
+ compactionOptions.compression = FLAGS_compression_type_e;
+ auto options = db->GetOptions();
+ MutableCFOptions mutable_cf_options(options);
+ for (size_t j = 0; j < sorted_runs[i].size(); j++) {
+ compactionOptions.output_file_size_limit = MaxFileSizeForLevel(
+ mutable_cf_options, static_cast<int>(output_level),
+ compaction_style);
+ std::cout << sorted_runs[i][j].size() << std::endl;
+ db->CompactFiles(
+ compactionOptions,
+ {sorted_runs[i][j].back().name, sorted_runs[i][j].front().name},
+ static_cast<int>(output_level - j) /*level*/);
+ }
+ }
+ } else if (compaction_style == kCompactionStyleUniversal) {
+ auto ratio = open_options_.compaction_options_universal.size_ratio;
+ bool should_stop = false;
+ while (!should_stop) {
+ if (sorted_runs[0].empty()) {
+ DoWrite(thread, write_mode);
+ } else {
+ DoWrite(thread, UNIQUE_RANDOM);
+ }
+ for (size_t i = 0; i < num_db; i++) {
+ auto db = db_list[i];
+ db->Flush(FlushOptions());
+ db->GetColumnFamilyMetaData(&meta);
+ if (num_files_at_level0[i] == meta.levels[0].files.size() ||
+ writes_ == 0) {
+ should_stop = true;
+ continue;
+ }
+ sorted_runs[i].emplace_back(
+ meta.levels[0].files.begin(),
+ meta.levels[0].files.end() - num_files_at_level0[i]);
+ num_files_at_level0[i] = meta.levels[0].files.size();
+ if (sorted_runs[i].back().size() == 1) {
+ should_stop = true;
+ continue;
+ }
+ num_files_at_level0[i] = meta.levels[0].files.size();
+ }
+ writes_ = static_cast<int64_t>(writes_ * static_cast<double>(100) /
+ (ratio + 200));
+ }
+ for (size_t i = 0; i < num_db; i++) {
+ if (sorted_runs[i].size() < num_levels) {
+ fprintf(stderr, "n is too small to fill %" ROCKSDB_PRIszt " levels\n",
+ num_levels);
+ exit(1);
+ }
+ }
+ for (size_t i = 0; i < num_db; i++) {
+ auto db = db_list[i];
+ auto compactionOptions = CompactionOptions();
+ compactionOptions.compression = FLAGS_compression_type_e;
+ auto options = db->GetOptions();
+ MutableCFOptions mutable_cf_options(options);
+ for (size_t j = 0; j < sorted_runs[i].size(); j++) {
+ compactionOptions.output_file_size_limit = MaxFileSizeForLevel(
+ mutable_cf_options, static_cast<int>(output_level),
+ compaction_style);
+ db->CompactFiles(
+ compactionOptions,
+ {sorted_runs[i][j].back().name, sorted_runs[i][j].front().name},
+ (output_level > j ? static_cast<int>(output_level - j)
+ : 0) /*level*/);
+ }
+ }
+ } else if (compaction_style == kCompactionStyleFIFO) {
+ if (num_levels != 1) {
+ return Status::InvalidArgument(
+ "num_levels should be 1 for FIFO compaction");
+ }
+ if (FLAGS_num_multi_db != 0) {
+ return Status::InvalidArgument("Doesn't support multiDB");
+ }
+ auto db = db_list[0];
+ std::vector<std::string> file_names;
+ while (true) {
+ if (sorted_runs[0].empty()) {
+ DoWrite(thread, write_mode);
+ } else {
+ DoWrite(thread, UNIQUE_RANDOM);
+ }
+ db->Flush(FlushOptions());
+ db->GetColumnFamilyMetaData(&meta);
+ auto total_size = meta.levels[0].size;
+ if (total_size >=
+ db->GetOptions().compaction_options_fifo.max_table_files_size) {
+ for (auto file_meta : meta.levels[0].files) {
+ file_names.emplace_back(file_meta.name);
+ }
+ break;
+ }
+ }
+ // TODO(shuzhang1989): Investigate why CompactFiles not working
+ // auto compactionOptions = CompactionOptions();
+ // db->CompactFiles(compactionOptions, file_names, 0);
+ auto compactionOptions = CompactRangeOptions();
+ db->CompactRange(compactionOptions, nullptr, nullptr);
+ } else {
+ fprintf(stdout,
+ "%-12s : skipped (-compaction_stype=kCompactionStyleNone)\n",
+ "filldeterministic");
+ return Status::InvalidArgument("None compaction is not supported");
+ }
+
+// Verify seqno and key range
+// Note: the seqno get changed at the max level by implementation
+// optimization, so skip the check of the max level.
+#ifndef NDEBUG
+ for (size_t k = 0; k < num_db; k++) {
+ auto db = db_list[k];
+ db->GetColumnFamilyMetaData(&meta);
+ // verify the number of sorted runs
+ if (compaction_style == kCompactionStyleLevel) {
+ assert(num_levels - 1 == sorted_runs[k].size());
+ } else if (compaction_style == kCompactionStyleUniversal) {
+ assert(meta.levels[0].files.size() + num_levels - 1 ==
+ sorted_runs[k].size());
+ } else if (compaction_style == kCompactionStyleFIFO) {
+ // TODO(gzh): FIFO compaction
+ db->GetColumnFamilyMetaData(&meta);
+ auto total_size = meta.levels[0].size;
+ assert(total_size <=
+ db->GetOptions().compaction_options_fifo.max_table_files_size);
+ break;
+ }
+
+ // verify smallest/largest seqno and key range of each sorted run
+ auto max_level = num_levels - 1;
+ int level;
+ for (size_t i = 0; i < sorted_runs[k].size(); i++) {
+ level = static_cast<int>(max_level - i);
+ SequenceNumber sorted_run_smallest_seqno = kMaxSequenceNumber;
+ SequenceNumber sorted_run_largest_seqno = 0;
+ std::string sorted_run_smallest_key, sorted_run_largest_key;
+ bool first_key = true;
+ for (auto fileMeta : sorted_runs[k][i]) {
+ sorted_run_smallest_seqno =
+ std::min(sorted_run_smallest_seqno, fileMeta.smallest_seqno);
+ sorted_run_largest_seqno =
+ std::max(sorted_run_largest_seqno, fileMeta.largest_seqno);
+ if (first_key ||
+ db->DefaultColumnFamily()->GetComparator()->Compare(
+ fileMeta.smallestkey, sorted_run_smallest_key) < 0) {
+ sorted_run_smallest_key = fileMeta.smallestkey;
+ }
+ if (first_key ||
+ db->DefaultColumnFamily()->GetComparator()->Compare(
+ fileMeta.largestkey, sorted_run_largest_key) > 0) {
+ sorted_run_largest_key = fileMeta.largestkey;
+ }
+ first_key = false;
+ }
+ if (compaction_style == kCompactionStyleLevel ||
+ (compaction_style == kCompactionStyleUniversal && level > 0)) {
+ SequenceNumber level_smallest_seqno = kMaxSequenceNumber;
+ SequenceNumber level_largest_seqno = 0;
+ for (auto fileMeta : meta.levels[level].files) {
+ level_smallest_seqno =
+ std::min(level_smallest_seqno, fileMeta.smallest_seqno);
+ level_largest_seqno =
+ std::max(level_largest_seqno, fileMeta.largest_seqno);
+ }
+ assert(sorted_run_smallest_key ==
+ meta.levels[level].files.front().smallestkey);
+ assert(sorted_run_largest_key ==
+ meta.levels[level].files.back().largestkey);
+ if (level != static_cast<int>(max_level)) {
+ // compaction at max_level would change sequence number
+ assert(sorted_run_smallest_seqno == level_smallest_seqno);
+ assert(sorted_run_largest_seqno == level_largest_seqno);
+ }
+ } else if (compaction_style == kCompactionStyleUniversal) {
+ // level <= 0 means sorted runs on level 0
+ auto level0_file =
+ meta.levels[0].files[sorted_runs[k].size() - 1 - i];
+ assert(sorted_run_smallest_key == level0_file.smallestkey);
+ assert(sorted_run_largest_key == level0_file.largestkey);
+ if (level != static_cast<int>(max_level)) {
+ assert(sorted_run_smallest_seqno == level0_file.smallest_seqno);
+ assert(sorted_run_largest_seqno == level0_file.largest_seqno);
+ }
+ }
+ }
+ }
+#endif
+ // print the size of each sorted_run
+ for (size_t k = 0; k < num_db; k++) {
+ auto db = db_list[k];
+ fprintf(stdout,
+ "---------------------- DB %" ROCKSDB_PRIszt
+ " LSM ---------------------\n",
+ k);
+ db->GetColumnFamilyMetaData(&meta);
+ for (auto& levelMeta : meta.levels) {
+ if (levelMeta.files.empty()) {
+ continue;
+ }
+ if (levelMeta.level == 0) {
+ for (auto& fileMeta : levelMeta.files) {
+ fprintf(stdout, "Level[%d]: %s(size: %" PRIi64 " bytes)\n",
+ levelMeta.level, fileMeta.name.c_str(), fileMeta.size);
+ }
+ } else {
+ fprintf(stdout, "Level[%d]: %s - %s(total size: %" PRIi64 " bytes)\n",
+ levelMeta.level, levelMeta.files.front().name.c_str(),
+ levelMeta.files.back().name.c_str(), levelMeta.size);
+ }
+ }
+ }
+ for (size_t i = 0; i < num_db; i++) {
+ db_list[i]->SetOptions(
+ {{"disable_auto_compactions",
+ std::to_string(options_list[i].disable_auto_compactions)},
+ {"level0_slowdown_writes_trigger",
+ std::to_string(options_list[i].level0_slowdown_writes_trigger)},
+ {"level0_stop_writes_trigger",
+ std::to_string(options_list[i].level0_stop_writes_trigger)}});
+ }
+ return Status::OK();
+#else
+ (void)thread;
+ (void)compaction_style;
+ (void)write_mode;
+ fprintf(stderr, "Rocksdb Lite doesn't support filldeterministic\n");
+ return Status::NotSupported(
+ "Rocksdb Lite doesn't support filldeterministic");
+#endif // ROCKSDB_LITE
+ }
+
+ void ReadSequential(ThreadState* thread) {
+ if (db_.db != nullptr) {
+ ReadSequential(thread, db_.db);
+ } else {
+ for (const auto& db_with_cfh : multi_dbs_) {
+ ReadSequential(thread, db_with_cfh.db);
+ }
+ }
+ }
+
+ void ReadSequential(ThreadState* thread, DB* db) {
+ ReadOptions options = read_options_;
+ std::unique_ptr<char[]> ts_guard;
+ Slice ts;
+ if (user_timestamp_size_ > 0) {
+ ts_guard.reset(new char[user_timestamp_size_]);
+ ts = mock_app_clock_->GetTimestampForRead(thread->rand, ts_guard.get());
+ options.timestamp = &ts;
+ }
+
+ options.adaptive_readahead = FLAGS_adaptive_readahead;
+ options.async_io = FLAGS_async_io;
+
+ Iterator* iter = db->NewIterator(options);
+ int64_t i = 0;
+ int64_t bytes = 0;
+ for (iter->SeekToFirst(); i < reads_ && iter->Valid(); iter->Next()) {
+ bytes += iter->key().size() + iter->value().size();
+ thread->stats.FinishedOps(nullptr, db, 1, kRead);
+ ++i;
+
+ if (thread->shared->read_rate_limiter.get() != nullptr &&
+ i % 1024 == 1023) {
+ thread->shared->read_rate_limiter->Request(1024, Env::IO_HIGH,
+ nullptr /* stats */,
+ RateLimiter::OpType::kRead);
+ }
+ }
+
+ delete iter;
+ thread->stats.AddBytes(bytes);
+ }
+
+ void ReadToRowCache(ThreadState* thread) {
+ int64_t read = 0;
+ int64_t found = 0;
+ int64_t bytes = 0;
+ int64_t key_rand = 0;
+ std::unique_ptr<const char[]> key_guard;
+ Slice key = AllocateKey(&key_guard);
+ PinnableSlice pinnable_val;
+
+ while (key_rand < FLAGS_num) {
+ DBWithColumnFamilies* db_with_cfh = SelectDBWithCfh(thread);
+ // We use same key_rand as seed for key and column family so that we can
+ // deterministically find the cfh corresponding to a particular key, as it
+ // is done in DoWrite method.
+ GenerateKeyFromInt(key_rand, FLAGS_num, &key);
+ key_rand++;
+ read++;
+ Status s;
+ if (FLAGS_num_column_families > 1) {
+ s = db_with_cfh->db->Get(read_options_, db_with_cfh->GetCfh(key_rand),
+ key, &pinnable_val);
+ } else {
+ pinnable_val.Reset();
+ s = db_with_cfh->db->Get(read_options_,
+ db_with_cfh->db->DefaultColumnFamily(), key,
+ &pinnable_val);
+ }
+
+ if (s.ok()) {
+ found++;
+ bytes += key.size() + pinnable_val.size();
+ } else if (!s.IsNotFound()) {
+ fprintf(stderr, "Get returned an error: %s\n", s.ToString().c_str());
+ abort();
+ }
+
+ if (thread->shared->read_rate_limiter.get() != nullptr &&
+ read % 256 == 255) {
+ thread->shared->read_rate_limiter->Request(
+ 256, Env::IO_HIGH, nullptr /* stats */, RateLimiter::OpType::kRead);
+ }
+
+ thread->stats.FinishedOps(db_with_cfh, db_with_cfh->db, 1, kRead);
+ }
+
+ char msg[100];
+ snprintf(msg, sizeof(msg), "(%" PRIu64 " of %" PRIu64 " found)\n", found,
+ read);
+
+ thread->stats.AddBytes(bytes);
+ thread->stats.AddMessage(msg);
+ }
+
+ void ReadReverse(ThreadState* thread) {
+ if (db_.db != nullptr) {
+ ReadReverse(thread, db_.db);
+ } else {
+ for (const auto& db_with_cfh : multi_dbs_) {
+ ReadReverse(thread, db_with_cfh.db);
+ }
+ }
+ }
+
+ void ReadReverse(ThreadState* thread, DB* db) {
+ Iterator* iter = db->NewIterator(read_options_);
+ int64_t i = 0;
+ int64_t bytes = 0;
+ for (iter->SeekToLast(); i < reads_ && iter->Valid(); iter->Prev()) {
+ bytes += iter->key().size() + iter->value().size();
+ thread->stats.FinishedOps(nullptr, db, 1, kRead);
+ ++i;
+ if (thread->shared->read_rate_limiter.get() != nullptr &&
+ i % 1024 == 1023) {
+ thread->shared->read_rate_limiter->Request(1024, Env::IO_HIGH,
+ nullptr /* stats */,
+ RateLimiter::OpType::kRead);
+ }
+ }
+ delete iter;
+ thread->stats.AddBytes(bytes);
+ }
+
+ void ReadRandomFast(ThreadState* thread) {
+ int64_t read = 0;
+ int64_t found = 0;
+ int64_t nonexist = 0;
+ ReadOptions options = read_options_;
+ std::unique_ptr<const char[]> key_guard;
+ Slice key = AllocateKey(&key_guard);
+ std::string value;
+ Slice ts;
+ std::unique_ptr<char[]> ts_guard;
+ if (user_timestamp_size_ > 0) {
+ ts_guard.reset(new char[user_timestamp_size_]);
+ }
+ DB* db = SelectDBWithCfh(thread)->db;
+
+ int64_t pot = 1;
+ while (pot < FLAGS_num) {
+ pot <<= 1;
+ }
+
+ Duration duration(FLAGS_duration, reads_);
+ do {
+ for (int i = 0; i < 100; ++i) {
+ int64_t key_rand = thread->rand.Next() & (pot - 1);
+ GenerateKeyFromInt(key_rand, FLAGS_num, &key);
+ ++read;
+ std::string ts_ret;
+ std::string* ts_ptr = nullptr;
+ if (user_timestamp_size_ > 0) {
+ ts = mock_app_clock_->GetTimestampForRead(thread->rand,
+ ts_guard.get());
+ options.timestamp = &ts;
+ ts_ptr = &ts_ret;
+ }
+ auto status = db->Get(options, key, &value, ts_ptr);
+ if (status.ok()) {
+ ++found;
+ } else if (!status.IsNotFound()) {
+ fprintf(stderr, "Get returned an error: %s\n",
+ status.ToString().c_str());
+ abort();
+ }
+ if (key_rand >= FLAGS_num) {
+ ++nonexist;
+ }
+ }
+ if (thread->shared->read_rate_limiter.get() != nullptr) {
+ thread->shared->read_rate_limiter->Request(
+ 100, Env::IO_HIGH, nullptr /* stats */, RateLimiter::OpType::kRead);
+ }
+
+ thread->stats.FinishedOps(nullptr, db, 100, kRead);
+ } while (!duration.Done(100));
+
+ char msg[100];
+ snprintf(msg, sizeof(msg),
+ "(%" PRIu64 " of %" PRIu64
+ " found, "
+ "issued %" PRIu64 " non-exist keys)\n",
+ found, read, nonexist);
+
+ thread->stats.AddMessage(msg);
+ }
+
+ int64_t GetRandomKey(Random64* rand) {
+ uint64_t rand_int = rand->Next();
+ int64_t key_rand;
+ if (read_random_exp_range_ == 0) {
+ key_rand = rand_int % FLAGS_num;
+ } else {
+ const uint64_t kBigInt = static_cast<uint64_t>(1U) << 62;
+ long double order = -static_cast<long double>(rand_int % kBigInt) /
+ static_cast<long double>(kBigInt) *
+ read_random_exp_range_;
+ long double exp_ran = std::exp(order);
+ uint64_t rand_num =
+ static_cast<int64_t>(exp_ran * static_cast<long double>(FLAGS_num));
+ // Map to a different number to avoid locality.
+ const uint64_t kBigPrime = 0x5bd1e995;
+ // Overflow is like %(2^64). Will have little impact of results.
+ key_rand = static_cast<int64_t>((rand_num * kBigPrime) % FLAGS_num);
+ }
+ return key_rand;
+ }
+
+ void ReadRandom(ThreadState* thread) {
+ int64_t read = 0;
+ int64_t found = 0;
+ int64_t bytes = 0;
+ int num_keys = 0;
+ int64_t key_rand = 0;
+ ReadOptions options = read_options_;
+ std::unique_ptr<const char[]> key_guard;
+ Slice key = AllocateKey(&key_guard);
+ PinnableSlice pinnable_val;
+ std::vector<PinnableSlice> pinnable_vals;
+ if (read_operands_) {
+ // Start off with a small-ish value that'll be increased later if
+ // `GetMergeOperands()` tells us it is not large enough.
+ pinnable_vals.resize(8);
+ }
+ std::unique_ptr<char[]> ts_guard;
+ Slice ts;
+ if (user_timestamp_size_ > 0) {
+ ts_guard.reset(new char[user_timestamp_size_]);
+ }
+
+ Duration duration(FLAGS_duration, reads_);
+ while (!duration.Done(1)) {
+ DBWithColumnFamilies* db_with_cfh = SelectDBWithCfh(thread);
+ // We use same key_rand as seed for key and column family so that we can
+ // deterministically find the cfh corresponding to a particular key, as it
+ // is done in DoWrite method.
+ if (entries_per_batch_ > 1 && FLAGS_multiread_stride) {
+ if (++num_keys == entries_per_batch_) {
+ num_keys = 0;
+ key_rand = GetRandomKey(&thread->rand);
+ if ((key_rand + (entries_per_batch_ - 1) * FLAGS_multiread_stride) >=
+ FLAGS_num) {
+ key_rand = FLAGS_num - entries_per_batch_ * FLAGS_multiread_stride;
+ }
+ } else {
+ key_rand += FLAGS_multiread_stride;
+ }
+ } else {
+ key_rand = GetRandomKey(&thread->rand);
+ }
+ GenerateKeyFromInt(key_rand, FLAGS_num, &key);
+ read++;
+ std::string ts_ret;
+ std::string* ts_ptr = nullptr;
+ if (user_timestamp_size_ > 0) {
+ ts = mock_app_clock_->GetTimestampForRead(thread->rand, ts_guard.get());
+ options.timestamp = &ts;
+ ts_ptr = &ts_ret;
+ }
+ Status s;
+ pinnable_val.Reset();
+ for (size_t i = 0; i < pinnable_vals.size(); ++i) {
+ pinnable_vals[i].Reset();
+ }
+ ColumnFamilyHandle* cfh;
+ if (FLAGS_num_column_families > 1) {
+ cfh = db_with_cfh->GetCfh(key_rand);
+ } else {
+ cfh = db_with_cfh->db->DefaultColumnFamily();
+ }
+ if (read_operands_) {
+ GetMergeOperandsOptions get_merge_operands_options;
+ get_merge_operands_options.expected_max_number_of_operands =
+ static_cast<int>(pinnable_vals.size());
+ int number_of_operands;
+ s = db_with_cfh->db->GetMergeOperands(
+ options, cfh, key, pinnable_vals.data(),
+ &get_merge_operands_options, &number_of_operands);
+ if (s.IsIncomplete()) {
+ // Should only happen a few times when we encounter a key that had
+ // more merge operands than any key seen so far. Production use case
+ // would typically retry in such event to get all the operands so do
+ // that here.
+ pinnable_vals.resize(number_of_operands);
+ get_merge_operands_options.expected_max_number_of_operands =
+ static_cast<int>(pinnable_vals.size());
+ s = db_with_cfh->db->GetMergeOperands(
+ options, cfh, key, pinnable_vals.data(),
+ &get_merge_operands_options, &number_of_operands);
+ }
+ } else {
+ s = db_with_cfh->db->Get(options, cfh, key, &pinnable_val, ts_ptr);
+ }
+
+ if (s.ok()) {
+ found++;
+ bytes += key.size() + pinnable_val.size() + user_timestamp_size_;
+ for (size_t i = 0; i < pinnable_vals.size(); ++i) {
+ bytes += pinnable_vals[i].size();
+ pinnable_vals[i].Reset();
+ }
+ } else if (!s.IsNotFound()) {
+ fprintf(stderr, "Get returned an error: %s\n", s.ToString().c_str());
+ abort();
+ }
+
+ if (thread->shared->read_rate_limiter.get() != nullptr &&
+ read % 256 == 255) {
+ thread->shared->read_rate_limiter->Request(
+ 256, Env::IO_HIGH, nullptr /* stats */, RateLimiter::OpType::kRead);
+ }
+
+ thread->stats.FinishedOps(db_with_cfh, db_with_cfh->db, 1, kRead);
+ }
+
+ char msg[100];
+ snprintf(msg, sizeof(msg), "(%" PRIu64 " of %" PRIu64 " found)\n", found,
+ read);
+
+ thread->stats.AddBytes(bytes);
+ thread->stats.AddMessage(msg);
+ }
+
+ // Calls MultiGet over a list of keys from a random distribution.
+ // Returns the total number of keys found.
+ void MultiReadRandom(ThreadState* thread) {
+ int64_t read = 0;
+ int64_t bytes = 0;
+ int64_t num_multireads = 0;
+ int64_t found = 0;
+ ReadOptions options = read_options_;
+ std::vector<Slice> keys;
+ std::vector<std::unique_ptr<const char[]>> key_guards;
+ std::vector<std::string> values(entries_per_batch_);
+ PinnableSlice* pin_values = new PinnableSlice[entries_per_batch_];
+ std::unique_ptr<PinnableSlice[]> pin_values_guard(pin_values);
+ std::vector<Status> stat_list(entries_per_batch_);
+ while (static_cast<int64_t>(keys.size()) < entries_per_batch_) {
+ key_guards.push_back(std::unique_ptr<const char[]>());
+ keys.push_back(AllocateKey(&key_guards.back()));
+ }
+
+ std::unique_ptr<char[]> ts_guard;
+ if (user_timestamp_size_ > 0) {
+ ts_guard.reset(new char[user_timestamp_size_]);
+ }
+
+ Duration duration(FLAGS_duration, reads_);
+ while (!duration.Done(entries_per_batch_)) {
+ DB* db = SelectDB(thread);
+ if (FLAGS_multiread_stride) {
+ int64_t key = GetRandomKey(&thread->rand);
+ if ((key + (entries_per_batch_ - 1) * FLAGS_multiread_stride) >=
+ static_cast<int64_t>(FLAGS_num)) {
+ key = FLAGS_num - entries_per_batch_ * FLAGS_multiread_stride;
+ }
+ for (int64_t i = 0; i < entries_per_batch_; ++i) {
+ GenerateKeyFromInt(key, FLAGS_num, &keys[i]);
+ key += FLAGS_multiread_stride;
+ }
+ } else {
+ for (int64_t i = 0; i < entries_per_batch_; ++i) {
+ GenerateKeyFromInt(GetRandomKey(&thread->rand), FLAGS_num, &keys[i]);
+ }
+ }
+ Slice ts;
+ if (user_timestamp_size_ > 0) {
+ ts = mock_app_clock_->GetTimestampForRead(thread->rand, ts_guard.get());
+ options.timestamp = &ts;
+ }
+ if (!FLAGS_multiread_batched) {
+ std::vector<Status> statuses = db->MultiGet(options, keys, &values);
+ assert(static_cast<int64_t>(statuses.size()) == entries_per_batch_);
+
+ read += entries_per_batch_;
+ num_multireads++;
+ for (int64_t i = 0; i < entries_per_batch_; ++i) {
+ if (statuses[i].ok()) {
+ bytes += keys[i].size() + values[i].size() + user_timestamp_size_;
+ ++found;
+ } else if (!statuses[i].IsNotFound()) {
+ fprintf(stderr, "MultiGet returned an error: %s\n",
+ statuses[i].ToString().c_str());
+ abort();
+ }
+ }
+ } else {
+ db->MultiGet(options, db->DefaultColumnFamily(), keys.size(),
+ keys.data(), pin_values, stat_list.data());
+
+ read += entries_per_batch_;
+ num_multireads++;
+ for (int64_t i = 0; i < entries_per_batch_; ++i) {
+ if (stat_list[i].ok()) {
+ bytes +=
+ keys[i].size() + pin_values[i].size() + user_timestamp_size_;
+ ++found;
+ } else if (!stat_list[i].IsNotFound()) {
+ fprintf(stderr, "MultiGet returned an error: %s\n",
+ stat_list[i].ToString().c_str());
+ abort();
+ }
+ stat_list[i] = Status::OK();
+ pin_values[i].Reset();
+ }
+ }
+ if (thread->shared->read_rate_limiter.get() != nullptr &&
+ num_multireads % 256 == 255) {
+ thread->shared->read_rate_limiter->Request(
+ 256 * entries_per_batch_, Env::IO_HIGH, nullptr /* stats */,
+ RateLimiter::OpType::kRead);
+ }
+ thread->stats.FinishedOps(nullptr, db, entries_per_batch_, kRead);
+ }
+
+ char msg[100];
+ snprintf(msg, sizeof(msg), "(%" PRIu64 " of %" PRIu64 " found)", found,
+ read);
+ thread->stats.AddBytes(bytes);
+ thread->stats.AddMessage(msg);
+ }
+
+ // Calls ApproximateSize over random key ranges.
+ void ApproximateSizeRandom(ThreadState* thread) {
+ int64_t size_sum = 0;
+ int64_t num_sizes = 0;
+ const size_t batch_size = entries_per_batch_;
+ std::vector<Range> ranges;
+ std::vector<Slice> lkeys;
+ std::vector<std::unique_ptr<const char[]>> lkey_guards;
+ std::vector<Slice> rkeys;
+ std::vector<std::unique_ptr<const char[]>> rkey_guards;
+ std::vector<uint64_t> sizes;
+ while (ranges.size() < batch_size) {
+ // Ugly without C++17 return from emplace_back
+ lkey_guards.emplace_back();
+ rkey_guards.emplace_back();
+ lkeys.emplace_back(AllocateKey(&lkey_guards.back()));
+ rkeys.emplace_back(AllocateKey(&rkey_guards.back()));
+ ranges.emplace_back(lkeys.back(), rkeys.back());
+ sizes.push_back(0);
+ }
+ Duration duration(FLAGS_duration, reads_);
+ while (!duration.Done(1)) {
+ DB* db = SelectDB(thread);
+ for (size_t i = 0; i < batch_size; ++i) {
+ int64_t lkey = GetRandomKey(&thread->rand);
+ int64_t rkey = GetRandomKey(&thread->rand);
+ if (lkey > rkey) {
+ std::swap(lkey, rkey);
+ }
+ GenerateKeyFromInt(lkey, FLAGS_num, &lkeys[i]);
+ GenerateKeyFromInt(rkey, FLAGS_num, &rkeys[i]);
+ }
+ db->GetApproximateSizes(&ranges[0], static_cast<int>(entries_per_batch_),
+ &sizes[0]);
+ num_sizes += entries_per_batch_;
+ for (int64_t size : sizes) {
+ size_sum += size;
+ }
+ thread->stats.FinishedOps(nullptr, db, entries_per_batch_, kOthers);
+ }
+
+ char msg[100];
+ snprintf(msg, sizeof(msg), "(Avg approx size=%g)",
+ static_cast<double>(size_sum) / static_cast<double>(num_sizes));
+ thread->stats.AddMessage(msg);
+ }
+
+ // The inverse function of Pareto distribution
+ int64_t ParetoCdfInversion(double u, double theta, double k, double sigma) {
+ double ret;
+ if (k == 0.0) {
+ ret = theta - sigma * std::log(u);
+ } else {
+ ret = theta + sigma * (std::pow(u, -1 * k) - 1) / k;
+ }
+ return static_cast<int64_t>(ceil(ret));
+ }
+ // The inverse function of power distribution (y=ax^b)
+ int64_t PowerCdfInversion(double u, double a, double b) {
+ double ret;
+ ret = std::pow((u / a), (1 / b));
+ return static_cast<int64_t>(ceil(ret));
+ }
+
+ // Add the noice to the QPS
+ double AddNoise(double origin, double noise_ratio) {
+ if (noise_ratio < 0.0 || noise_ratio > 1.0) {
+ return origin;
+ }
+ int band_int = static_cast<int>(FLAGS_sine_a);
+ double delta = (rand() % band_int - band_int / 2) * noise_ratio;
+ if (origin + delta < 0) {
+ return origin;
+ } else {
+ return (origin + delta);
+ }
+ }
+
+ // Decide the ratio of different query types
+ // 0 Get, 1 Put, 2 Seek, 3 SeekForPrev, 4 Delete, 5 SingleDelete, 6 merge
+ class QueryDecider {
+ public:
+ std::vector<int> type_;
+ std::vector<double> ratio_;
+ int range_;
+
+ QueryDecider() {}
+ ~QueryDecider() {}
+
+ Status Initiate(std::vector<double> ratio_input) {
+ int range_max = 1000;
+ double sum = 0.0;
+ for (auto& ratio : ratio_input) {
+ sum += ratio;
+ }
+ range_ = 0;
+ for (auto& ratio : ratio_input) {
+ range_ += static_cast<int>(ceil(range_max * (ratio / sum)));
+ type_.push_back(range_);
+ ratio_.push_back(ratio / sum);
+ }
+ return Status::OK();
+ }
+
+ int GetType(int64_t rand_num) {
+ if (rand_num < 0) {
+ rand_num = rand_num * (-1);
+ }
+ assert(range_ != 0);
+ int pos = static_cast<int>(rand_num % range_);
+ for (int i = 0; i < static_cast<int>(type_.size()); i++) {
+ if (pos < type_[i]) {
+ return i;
+ }
+ }
+ return 0;
+ }
+ };
+
+ // KeyrangeUnit is the struct of a keyrange. It is used in a keyrange vector
+ // to transfer a random value to one keyrange based on the hotness.
+ struct KeyrangeUnit {
+ int64_t keyrange_start;
+ int64_t keyrange_access;
+ int64_t keyrange_keys;
+ };
+
+ // From our observations, the prefix hotness (key-range hotness) follows
+ // the two-term-exponential distribution: f(x) = a*exp(b*x) + c*exp(d*x).
+ // However, we cannot directly use the inverse function to decide a
+ // key-range from a random distribution. To achieve it, we create a list of
+ // KeyrangeUnit, each KeyrangeUnit occupies a range of integers whose size is
+ // decided based on the hotness of the key-range. When a random value is
+ // generated based on uniform distribution, we map it to the KeyrangeUnit Vec
+ // and one KeyrangeUnit is selected. The probability of a KeyrangeUnit being
+ // selected is the same as the hotness of this KeyrangeUnit. After that, the
+ // key can be randomly allocated to the key-range of this KeyrangeUnit, or we
+ // can based on the power distribution (y=ax^b) to generate the offset of
+ // the key in the selected key-range. In this way, we generate the keyID
+ // based on the hotness of the prefix and also the key hotness distribution.
+ class GenerateTwoTermExpKeys {
+ public:
+ // Avoid uninitialized warning-as-error in some compilers
+ int64_t keyrange_rand_max_ = 0;
+ int64_t keyrange_size_ = 0;
+ int64_t keyrange_num_ = 0;
+ std::vector<KeyrangeUnit> keyrange_set_;
+
+ // Initiate the KeyrangeUnit vector and calculate the size of each
+ // KeyrangeUnit.
+ Status InitiateExpDistribution(int64_t total_keys, double prefix_a,
+ double prefix_b, double prefix_c,
+ double prefix_d) {
+ int64_t amplify = 0;
+ int64_t keyrange_start = 0;
+ if (FLAGS_keyrange_num <= 0) {
+ keyrange_num_ = 1;
+ } else {
+ keyrange_num_ = FLAGS_keyrange_num;
+ }
+ keyrange_size_ = total_keys / keyrange_num_;
+
+ // Calculate the key-range shares size based on the input parameters
+ for (int64_t pfx = keyrange_num_; pfx >= 1; pfx--) {
+ // Step 1. Calculate the probability that this key range will be
+ // accessed in a query. It is based on the two-term expoential
+ // distribution
+ double keyrange_p = prefix_a * std::exp(prefix_b * pfx) +
+ prefix_c * std::exp(prefix_d * pfx);
+ if (keyrange_p < std::pow(10.0, -16.0)) {
+ keyrange_p = 0.0;
+ }
+ // Step 2. Calculate the amplify
+ // In order to allocate a query to a key-range based on the random
+ // number generated for this query, we need to extend the probability
+ // of each key range from [0,1] to [0, amplify]. Amplify is calculated
+ // by 1/(smallest key-range probability). In this way, we ensure that
+ // all key-ranges are assigned with an Integer that >=0
+ if (amplify == 0 && keyrange_p > 0) {
+ amplify = static_cast<int64_t>(std::floor(1 / keyrange_p)) + 1;
+ }
+
+ // Step 3. For each key-range, we calculate its position in the
+ // [0, amplify] range, including the start, the size (keyrange_access)
+ KeyrangeUnit p_unit;
+ p_unit.keyrange_start = keyrange_start;
+ if (0.0 >= keyrange_p) {
+ p_unit.keyrange_access = 0;
+ } else {
+ p_unit.keyrange_access =
+ static_cast<int64_t>(std::floor(amplify * keyrange_p));
+ }
+ p_unit.keyrange_keys = keyrange_size_;
+ keyrange_set_.push_back(p_unit);
+ keyrange_start += p_unit.keyrange_access;
+ }
+ keyrange_rand_max_ = keyrange_start;
+
+ // Step 4. Shuffle the key-ranges randomly
+ // Since the access probability is calculated from small to large,
+ // If we do not re-allocate them, hot key-ranges are always at the end
+ // and cold key-ranges are at the begin of the key space. Therefore, the
+ // key-ranges are shuffled and the rand seed is only decide by the
+ // key-range hotness distribution. With the same distribution parameters
+ // the shuffle results are the same.
+ Random64 rand_loca(keyrange_rand_max_);
+ for (int64_t i = 0; i < FLAGS_keyrange_num; i++) {
+ int64_t pos = rand_loca.Next() % FLAGS_keyrange_num;
+ assert(i >= 0 && i < static_cast<int64_t>(keyrange_set_.size()) &&
+ pos >= 0 && pos < static_cast<int64_t>(keyrange_set_.size()));
+ std::swap(keyrange_set_[i], keyrange_set_[pos]);
+ }
+
+ // Step 5. Recalculate the prefix start postion after shuffling
+ int64_t offset = 0;
+ for (auto& p_unit : keyrange_set_) {
+ p_unit.keyrange_start = offset;
+ offset += p_unit.keyrange_access;
+ }
+
+ return Status::OK();
+ }
+
+ // Generate the Key ID according to the input ini_rand and key distribution
+ int64_t DistGetKeyID(int64_t ini_rand, double key_dist_a,
+ double key_dist_b) {
+ int64_t keyrange_rand = ini_rand % keyrange_rand_max_;
+
+ // Calculate and select one key-range that contains the new key
+ int64_t start = 0, end = static_cast<int64_t>(keyrange_set_.size());
+ while (start + 1 < end) {
+ int64_t mid = start + (end - start) / 2;
+ assert(mid >= 0 && mid < static_cast<int64_t>(keyrange_set_.size()));
+ if (keyrange_rand < keyrange_set_[mid].keyrange_start) {
+ end = mid;
+ } else {
+ start = mid;
+ }
+ }
+ int64_t keyrange_id = start;
+
+ // Select one key in the key-range and compose the keyID
+ int64_t key_offset = 0, key_seed;
+ if (key_dist_a == 0.0 || key_dist_b == 0.0) {
+ key_offset = ini_rand % keyrange_size_;
+ } else {
+ double u =
+ static_cast<double>(ini_rand % keyrange_size_) / keyrange_size_;
+ key_seed = static_cast<int64_t>(
+ ceil(std::pow((u / key_dist_a), (1 / key_dist_b))));
+ Random64 rand_key(key_seed);
+ key_offset = rand_key.Next() % keyrange_size_;
+ }
+ return keyrange_size_ * keyrange_id + key_offset;
+ }
+ };
+
+ // The social graph workload mixed with Get, Put, Iterator queries.
+ // The value size and iterator length follow Pareto distribution.
+ // The overall key access follow power distribution. If user models the
+ // workload based on different key-ranges (or different prefixes), user
+ // can use two-term-exponential distribution to fit the workload. User
+ // needs to decide the ratio between Get, Put, Iterator queries before
+ // starting the benchmark.
+ void MixGraph(ThreadState* thread) {
+ int64_t gets = 0;
+ int64_t puts = 0;
+ int64_t get_found = 0;
+ int64_t seek = 0;
+ int64_t seek_found = 0;
+ int64_t bytes = 0;
+ double total_scan_length = 0;
+ double total_val_size = 0;
+ const int64_t default_value_max = 1 * 1024 * 1024;
+ int64_t value_max = default_value_max;
+ int64_t scan_len_max = FLAGS_mix_max_scan_len;
+ double write_rate = 1000000.0;
+ double read_rate = 1000000.0;
+ bool use_prefix_modeling = false;
+ bool use_random_modeling = false;
+ GenerateTwoTermExpKeys gen_exp;
+ std::vector<double> ratio{FLAGS_mix_get_ratio, FLAGS_mix_put_ratio,
+ FLAGS_mix_seek_ratio};
+ char value_buffer[default_value_max];
+ QueryDecider query;
+ RandomGenerator gen;
+ Status s;
+ if (value_max > FLAGS_mix_max_value_size) {
+ value_max = FLAGS_mix_max_value_size;
+ }
+
+ std::unique_ptr<const char[]> key_guard;
+ Slice key = AllocateKey(&key_guard);
+ PinnableSlice pinnable_val;
+ query.Initiate(ratio);
+
+ // the limit of qps initiation
+ if (FLAGS_sine_mix_rate) {
+ thread->shared->read_rate_limiter.reset(
+ NewGenericRateLimiter(static_cast<int64_t>(read_rate)));
+ thread->shared->write_rate_limiter.reset(
+ NewGenericRateLimiter(static_cast<int64_t>(write_rate)));
+ }
+
+ // Decide if user wants to use prefix based key generation
+ if (FLAGS_keyrange_dist_a != 0.0 || FLAGS_keyrange_dist_b != 0.0 ||
+ FLAGS_keyrange_dist_c != 0.0 || FLAGS_keyrange_dist_d != 0.0) {
+ use_prefix_modeling = true;
+ gen_exp.InitiateExpDistribution(
+ FLAGS_num, FLAGS_keyrange_dist_a, FLAGS_keyrange_dist_b,
+ FLAGS_keyrange_dist_c, FLAGS_keyrange_dist_d);
+ }
+ if (FLAGS_key_dist_a == 0 || FLAGS_key_dist_b == 0) {
+ use_random_modeling = true;
+ }
+
+ Duration duration(FLAGS_duration, reads_);
+ while (!duration.Done(1)) {
+ DBWithColumnFamilies* db_with_cfh = SelectDBWithCfh(thread);
+ int64_t ini_rand, rand_v, key_rand, key_seed;
+ ini_rand = GetRandomKey(&thread->rand);
+ rand_v = ini_rand % FLAGS_num;
+ double u = static_cast<double>(rand_v) / FLAGS_num;
+
+ // Generate the keyID based on the key hotness and prefix hotness
+ if (use_random_modeling) {
+ key_rand = ini_rand;
+ } else if (use_prefix_modeling) {
+ key_rand =
+ gen_exp.DistGetKeyID(ini_rand, FLAGS_key_dist_a, FLAGS_key_dist_b);
+ } else {
+ key_seed = PowerCdfInversion(u, FLAGS_key_dist_a, FLAGS_key_dist_b);
+ Random64 rand(key_seed);
+ key_rand = static_cast<int64_t>(rand.Next()) % FLAGS_num;
+ }
+ GenerateKeyFromInt(key_rand, FLAGS_num, &key);
+ int query_type = query.GetType(rand_v);
+
+ // change the qps
+ uint64_t now = FLAGS_env->NowMicros();
+ uint64_t usecs_since_last;
+ if (now > thread->stats.GetSineInterval()) {
+ usecs_since_last = now - thread->stats.GetSineInterval();
+ } else {
+ usecs_since_last = 0;
+ }
+
+ if (FLAGS_sine_mix_rate &&
+ usecs_since_last >
+ (FLAGS_sine_mix_rate_interval_milliseconds * uint64_t{1000})) {
+ double usecs_since_start =
+ static_cast<double>(now - thread->stats.GetStart());
+ thread->stats.ResetSineInterval();
+ double mix_rate_with_noise = AddNoise(
+ SineRate(usecs_since_start / 1000000.0), FLAGS_sine_mix_rate_noise);
+ read_rate = mix_rate_with_noise * (query.ratio_[0] + query.ratio_[2]);
+ write_rate = mix_rate_with_noise * query.ratio_[1];
+
+ if (read_rate > 0) {
+ thread->shared->read_rate_limiter->SetBytesPerSecond(
+ static_cast<int64_t>(read_rate));
+ }
+ if (write_rate > 0) {
+ thread->shared->write_rate_limiter->SetBytesPerSecond(
+ static_cast<int64_t>(write_rate));
+ }
+ }
+ // Start the query
+ if (query_type == 0) {
+ // the Get query
+ gets++;
+ if (FLAGS_num_column_families > 1) {
+ s = db_with_cfh->db->Get(read_options_, db_with_cfh->GetCfh(key_rand),
+ key, &pinnable_val);
+ } else {
+ pinnable_val.Reset();
+ s = db_with_cfh->db->Get(read_options_,
+ db_with_cfh->db->DefaultColumnFamily(), key,
+ &pinnable_val);
+ }
+
+ if (s.ok()) {
+ get_found++;
+ bytes += key.size() + pinnable_val.size();
+ } else if (!s.IsNotFound()) {
+ fprintf(stderr, "Get returned an error: %s\n", s.ToString().c_str());
+ abort();
+ }
+
+ if (thread->shared->read_rate_limiter && (gets + seek) % 100 == 0) {
+ thread->shared->read_rate_limiter->Request(100, Env::IO_HIGH,
+ nullptr /*stats*/);
+ }
+ thread->stats.FinishedOps(db_with_cfh, db_with_cfh->db, 1, kRead);
+ } else if (query_type == 1) {
+ // the Put query
+ puts++;
+ int64_t val_size = ParetoCdfInversion(u, FLAGS_value_theta,
+ FLAGS_value_k, FLAGS_value_sigma);
+ if (val_size < 10) {
+ val_size = 10;
+ } else if (val_size > value_max) {
+ val_size = val_size % value_max;
+ }
+ total_val_size += val_size;
+
+ s = db_with_cfh->db->Put(
+ write_options_, key,
+ gen.Generate(static_cast<unsigned int>(val_size)));
+ if (!s.ok()) {
+ fprintf(stderr, "put error: %s\n", s.ToString().c_str());
+ ErrorExit();
+ }
+
+ if (thread->shared->write_rate_limiter && puts % 100 == 0) {
+ thread->shared->write_rate_limiter->Request(100, Env::IO_HIGH,
+ nullptr /*stats*/);
+ }
+ thread->stats.FinishedOps(db_with_cfh, db_with_cfh->db, 1, kWrite);
+ } else if (query_type == 2) {
+ // Seek query
+ if (db_with_cfh->db != nullptr) {
+ Iterator* single_iter = nullptr;
+ single_iter = db_with_cfh->db->NewIterator(read_options_);
+ if (single_iter != nullptr) {
+ single_iter->Seek(key);
+ seek++;
+ if (single_iter->Valid() && single_iter->key().compare(key) == 0) {
+ seek_found++;
+ }
+ int64_t scan_length =
+ ParetoCdfInversion(u, FLAGS_iter_theta, FLAGS_iter_k,
+ FLAGS_iter_sigma) %
+ scan_len_max;
+ for (int64_t j = 0; j < scan_length && single_iter->Valid(); j++) {
+ Slice value = single_iter->value();
+ memcpy(value_buffer, value.data(),
+ std::min(value.size(), sizeof(value_buffer)));
+ bytes += single_iter->key().size() + single_iter->value().size();
+ single_iter->Next();
+ assert(single_iter->status().ok());
+ total_scan_length++;
+ }
+ }
+ delete single_iter;
+ }
+ thread->stats.FinishedOps(db_with_cfh, db_with_cfh->db, 1, kSeek);
+ }
+ }
+ char msg[256];
+ snprintf(msg, sizeof(msg),
+ "( Gets:%" PRIu64 " Puts:%" PRIu64 " Seek:%" PRIu64
+ ", reads %" PRIu64 " in %" PRIu64
+ " found, "
+ "avg size: %.1f value, %.1f scan)\n",
+ gets, puts, seek, get_found + seek_found, gets + seek,
+ total_val_size / puts, total_scan_length / seek);
+
+ thread->stats.AddBytes(bytes);
+ thread->stats.AddMessage(msg);
+ }
+
+ void IteratorCreation(ThreadState* thread) {
+ Duration duration(FLAGS_duration, reads_);
+ ReadOptions options = read_options_;
+ std::unique_ptr<char[]> ts_guard;
+ if (user_timestamp_size_ > 0) {
+ ts_guard.reset(new char[user_timestamp_size_]);
+ }
+ while (!duration.Done(1)) {
+ DB* db = SelectDB(thread);
+ Slice ts;
+ if (user_timestamp_size_ > 0) {
+ ts = mock_app_clock_->GetTimestampForRead(thread->rand, ts_guard.get());
+ options.timestamp = &ts;
+ }
+ Iterator* iter = db->NewIterator(options);
+ delete iter;
+ thread->stats.FinishedOps(nullptr, db, 1, kOthers);
+ }
+ }
+
+ void IteratorCreationWhileWriting(ThreadState* thread) {
+ if (thread->tid > 0) {
+ IteratorCreation(thread);
+ } else {
+ BGWriter(thread, kWrite);
+ }
+ }
+
+ void SeekRandom(ThreadState* thread) {
+ int64_t read = 0;
+ int64_t found = 0;
+ int64_t bytes = 0;
+ ReadOptions options = read_options_;
+ std::unique_ptr<char[]> ts_guard;
+ Slice ts;
+ if (user_timestamp_size_ > 0) {
+ ts_guard.reset(new char[user_timestamp_size_]);
+ ts = mock_app_clock_->GetTimestampForRead(thread->rand, ts_guard.get());
+ options.timestamp = &ts;
+ }
+
+ std::vector<Iterator*> tailing_iters;
+ if (FLAGS_use_tailing_iterator) {
+ if (db_.db != nullptr) {
+ tailing_iters.push_back(db_.db->NewIterator(options));
+ } else {
+ for (const auto& db_with_cfh : multi_dbs_) {
+ tailing_iters.push_back(db_with_cfh.db->NewIterator(options));
+ }
+ }
+ }
+ options.auto_prefix_mode = FLAGS_auto_prefix_mode;
+
+ std::unique_ptr<const char[]> key_guard;
+ Slice key = AllocateKey(&key_guard);
+
+ std::unique_ptr<const char[]> upper_bound_key_guard;
+ Slice upper_bound = AllocateKey(&upper_bound_key_guard);
+ std::unique_ptr<const char[]> lower_bound_key_guard;
+ Slice lower_bound = AllocateKey(&lower_bound_key_guard);
+
+ Duration duration(FLAGS_duration, reads_);
+ char value_buffer[256];
+ while (!duration.Done(1)) {
+ int64_t seek_pos = thread->rand.Next() % FLAGS_num;
+ GenerateKeyFromIntForSeek(static_cast<uint64_t>(seek_pos), FLAGS_num,
+ &key);
+ if (FLAGS_max_scan_distance != 0) {
+ if (FLAGS_reverse_iterator) {
+ GenerateKeyFromInt(
+ static_cast<uint64_t>(std::max(
+ static_cast<int64_t>(0), seek_pos - FLAGS_max_scan_distance)),
+ FLAGS_num, &lower_bound);
+ options.iterate_lower_bound = &lower_bound;
+ } else {
+ auto min_num =
+ std::min(FLAGS_num, seek_pos + FLAGS_max_scan_distance);
+ GenerateKeyFromInt(static_cast<uint64_t>(min_num), FLAGS_num,
+ &upper_bound);
+ options.iterate_upper_bound = &upper_bound;
+ }
+ } else if (FLAGS_auto_prefix_mode && prefix_extractor_ &&
+ !FLAGS_reverse_iterator) {
+ // Set upper bound to next prefix
+ auto mutable_upper_bound = const_cast<char*>(upper_bound.data());
+ std::memcpy(mutable_upper_bound, key.data(), prefix_size_);
+ mutable_upper_bound[prefix_size_ - 1]++;
+ upper_bound = Slice(upper_bound.data(), prefix_size_);
+ options.iterate_upper_bound = &upper_bound;
+ }
+
+ // Pick a Iterator to use
+ uint64_t db_idx_to_use =
+ (db_.db == nullptr)
+ ? (uint64_t{thread->rand.Next()} % multi_dbs_.size())
+ : 0;
+ std::unique_ptr<Iterator> single_iter;
+ Iterator* iter_to_use;
+ if (FLAGS_use_tailing_iterator) {
+ iter_to_use = tailing_iters[db_idx_to_use];
+ } else {
+ if (db_.db != nullptr) {
+ single_iter.reset(db_.db->NewIterator(options));
+ } else {
+ single_iter.reset(multi_dbs_[db_idx_to_use].db->NewIterator(options));
+ }
+ iter_to_use = single_iter.get();
+ }
+
+ iter_to_use->Seek(key);
+ read++;
+ if (iter_to_use->Valid() && iter_to_use->key().compare(key) == 0) {
+ found++;
+ }
+
+ for (int j = 0; j < FLAGS_seek_nexts && iter_to_use->Valid(); ++j) {
+ // Copy out iterator's value to make sure we read them.
+ Slice value = iter_to_use->value();
+ memcpy(value_buffer, value.data(),
+ std::min(value.size(), sizeof(value_buffer)));
+ bytes += iter_to_use->key().size() + iter_to_use->value().size();
+
+ if (!FLAGS_reverse_iterator) {
+ iter_to_use->Next();
+ } else {
+ iter_to_use->Prev();
+ }
+ assert(iter_to_use->status().ok());
+ }
+
+ if (thread->shared->read_rate_limiter.get() != nullptr &&
+ read % 256 == 255) {
+ thread->shared->read_rate_limiter->Request(
+ 256, Env::IO_HIGH, nullptr /* stats */, RateLimiter::OpType::kRead);
+ }
+
+ thread->stats.FinishedOps(&db_, db_.db, 1, kSeek);
+ }
+ for (auto iter : tailing_iters) {
+ delete iter;
+ }
+
+ char msg[100];
+ snprintf(msg, sizeof(msg), "(%" PRIu64 " of %" PRIu64 " found)\n", found,
+ read);
+ thread->stats.AddBytes(bytes);
+ thread->stats.AddMessage(msg);
+ }
+
+ void SeekRandomWhileWriting(ThreadState* thread) {
+ if (thread->tid > 0) {
+ SeekRandom(thread);
+ } else {
+ BGWriter(thread, kWrite);
+ }
+ }
+
+ void SeekRandomWhileMerging(ThreadState* thread) {
+ if (thread->tid > 0) {
+ SeekRandom(thread);
+ } else {
+ BGWriter(thread, kMerge);
+ }
+ }
+
+ void DoDelete(ThreadState* thread, bool seq) {
+ WriteBatch batch(/*reserved_bytes=*/0, /*max_bytes=*/0,
+ FLAGS_write_batch_protection_bytes_per_key,
+ user_timestamp_size_);
+ Duration duration(seq ? 0 : FLAGS_duration, deletes_);
+ int64_t i = 0;
+ std::unique_ptr<const char[]> key_guard;
+ Slice key = AllocateKey(&key_guard);
+ std::unique_ptr<char[]> ts_guard;
+ Slice ts;
+ if (user_timestamp_size_ > 0) {
+ ts_guard.reset(new char[user_timestamp_size_]);
+ }
+
+ while (!duration.Done(entries_per_batch_)) {
+ DB* db = SelectDB(thread);
+ batch.Clear();
+ for (int64_t j = 0; j < entries_per_batch_; ++j) {
+ const int64_t k = seq ? i + j : (thread->rand.Next() % FLAGS_num);
+ GenerateKeyFromInt(k, FLAGS_num, &key);
+ batch.Delete(key);
+ }
+ Status s;
+ if (user_timestamp_size_ > 0) {
+ ts = mock_app_clock_->Allocate(ts_guard.get());
+ s = batch.UpdateTimestamps(
+ ts, [this](uint32_t) { return user_timestamp_size_; });
+ if (!s.ok()) {
+ fprintf(stderr, "assign timestamp: %s\n", s.ToString().c_str());
+ ErrorExit();
+ }
+ }
+ s = db->Write(write_options_, &batch);
+ thread->stats.FinishedOps(nullptr, db, entries_per_batch_, kDelete);
+ if (!s.ok()) {
+ fprintf(stderr, "del error: %s\n", s.ToString().c_str());
+ exit(1);
+ }
+ i += entries_per_batch_;
+ }
+ }
+
+ void DeleteSeq(ThreadState* thread) { DoDelete(thread, true); }
+
+ void DeleteRandom(ThreadState* thread) { DoDelete(thread, false); }
+
+ void ReadWhileWriting(ThreadState* thread) {
+ if (thread->tid > 0) {
+ ReadRandom(thread);
+ } else {
+ BGWriter(thread, kWrite);
+ }
+ }
+
+ void MultiReadWhileWriting(ThreadState* thread) {
+ if (thread->tid > 0) {
+ MultiReadRandom(thread);
+ } else {
+ BGWriter(thread, kWrite);
+ }
+ }
+
+ void ReadWhileMerging(ThreadState* thread) {
+ if (thread->tid > 0) {
+ ReadRandom(thread);
+ } else {
+ BGWriter(thread, kMerge);
+ }
+ }
+
+ void BGWriter(ThreadState* thread, enum OperationType write_merge) {
+ // Special thread that keeps writing until other threads are done.
+ RandomGenerator gen;
+ int64_t bytes = 0;
+
+ std::unique_ptr<RateLimiter> write_rate_limiter;
+ if (FLAGS_benchmark_write_rate_limit > 0) {
+ write_rate_limiter.reset(
+ NewGenericRateLimiter(FLAGS_benchmark_write_rate_limit));
+ }
+
+ // Don't merge stats from this thread with the readers.
+ thread->stats.SetExcludeFromMerge();
+
+ std::unique_ptr<const char[]> key_guard;
+ Slice key = AllocateKey(&key_guard);
+ std::unique_ptr<char[]> ts_guard;
+ std::unique_ptr<const char[]> begin_key_guard;
+ Slice begin_key = AllocateKey(&begin_key_guard);
+ std::unique_ptr<const char[]> end_key_guard;
+ Slice end_key = AllocateKey(&end_key_guard);
+ uint64_t num_range_deletions = 0;
+ std::vector<std::unique_ptr<const char[]>> expanded_key_guards;
+ std::vector<Slice> expanded_keys;
+ if (FLAGS_expand_range_tombstones) {
+ expanded_key_guards.resize(range_tombstone_width_);
+ for (auto& expanded_key_guard : expanded_key_guards) {
+ expanded_keys.emplace_back(AllocateKey(&expanded_key_guard));
+ }
+ }
+ if (user_timestamp_size_ > 0) {
+ ts_guard.reset(new char[user_timestamp_size_]);
+ }
+ uint32_t written = 0;
+ bool hint_printed = false;
+
+ while (true) {
+ DB* db = SelectDB(thread);
+ {
+ MutexLock l(&thread->shared->mu);
+ if (FLAGS_finish_after_writes && written == writes_) {
+ fprintf(stderr, "Exiting the writer after %u writes...\n", written);
+ break;
+ }
+ if (thread->shared->num_done + 1 >= thread->shared->num_initialized) {
+ // Other threads have finished
+ if (FLAGS_finish_after_writes) {
+ // Wait for the writes to be finished
+ if (!hint_printed) {
+ fprintf(stderr, "Reads are finished. Have %d more writes to do\n",
+ static_cast<int>(writes_) - written);
+ hint_printed = true;
+ }
+ } else {
+ // Finish the write immediately
+ break;
+ }
+ }
+ }
+
+ GenerateKeyFromInt(thread->rand.Next() % FLAGS_num, FLAGS_num, &key);
+ Status s;
+
+ Slice val = gen.Generate();
+ Slice ts;
+ if (user_timestamp_size_ > 0) {
+ ts = mock_app_clock_->Allocate(ts_guard.get());
+ }
+ if (write_merge == kWrite) {
+ if (user_timestamp_size_ == 0) {
+ s = db->Put(write_options_, key, val);
+ } else {
+ s = db->Put(write_options_, key, ts, val);
+ }
+ } else {
+ s = db->Merge(write_options_, key, val);
+ }
+ // Restore write_options_
+ written++;
+
+ if (!s.ok()) {
+ fprintf(stderr, "put or merge error: %s\n", s.ToString().c_str());
+ exit(1);
+ }
+ bytes += key.size() + val.size() + user_timestamp_size_;
+ thread->stats.FinishedOps(&db_, db_.db, 1, kWrite);
+
+ if (FLAGS_benchmark_write_rate_limit > 0) {
+ write_rate_limiter->Request(key.size() + val.size(), Env::IO_HIGH,
+ nullptr /* stats */,
+ RateLimiter::OpType::kWrite);
+ }
+
+ if (writes_per_range_tombstone_ > 0 &&
+ written > writes_before_delete_range_ &&
+ (written - writes_before_delete_range_) /
+ writes_per_range_tombstone_ <=
+ max_num_range_tombstones_ &&
+ (written - writes_before_delete_range_) %
+ writes_per_range_tombstone_ ==
+ 0) {
+ num_range_deletions++;
+ int64_t begin_num = thread->rand.Next() % FLAGS_num;
+ if (FLAGS_expand_range_tombstones) {
+ for (int64_t offset = 0; offset < range_tombstone_width_; ++offset) {
+ GenerateKeyFromInt(begin_num + offset, FLAGS_num,
+ &expanded_keys[offset]);
+ if (!db->Delete(write_options_, expanded_keys[offset]).ok()) {
+ fprintf(stderr, "delete error: %s\n", s.ToString().c_str());
+ exit(1);
+ }
+ }
+ } else {
+ GenerateKeyFromInt(begin_num, FLAGS_num, &begin_key);
+ GenerateKeyFromInt(begin_num + range_tombstone_width_, FLAGS_num,
+ &end_key);
+ if (!db->DeleteRange(write_options_, db->DefaultColumnFamily(),
+ begin_key, end_key)
+ .ok()) {
+ fprintf(stderr, "deleterange error: %s\n", s.ToString().c_str());
+ exit(1);
+ }
+ }
+ thread->stats.FinishedOps(&db_, db_.db, 1, kWrite);
+ // TODO: DeleteRange is not included in calculcation of bytes/rate
+ // limiter request
+ }
+ }
+ if (num_range_deletions > 0) {
+ std::cout << "Number of range deletions: " << num_range_deletions
+ << std::endl;
+ }
+ thread->stats.AddBytes(bytes);
+ }
+
+ void ReadWhileScanning(ThreadState* thread) {
+ if (thread->tid > 0) {
+ ReadRandom(thread);
+ } else {
+ BGScan(thread);
+ }
+ }
+
+ void BGScan(ThreadState* thread) {
+ if (FLAGS_num_multi_db > 0) {
+ fprintf(stderr, "Not supporting multiple DBs.\n");
+ abort();
+ }
+ assert(db_.db != nullptr);
+ ReadOptions read_options = read_options_;
+ std::unique_ptr<char[]> ts_guard;
+ Slice ts;
+ if (user_timestamp_size_ > 0) {
+ ts_guard.reset(new char[user_timestamp_size_]);
+ ts = mock_app_clock_->GetTimestampForRead(thread->rand, ts_guard.get());
+ read_options.timestamp = &ts;
+ }
+ Iterator* iter = db_.db->NewIterator(read_options);
+
+ fprintf(stderr, "num reads to do %" PRIu64 "\n", reads_);
+ Duration duration(FLAGS_duration, reads_);
+ uint64_t num_seek_to_first = 0;
+ uint64_t num_next = 0;
+ while (!duration.Done(1)) {
+ if (!iter->Valid()) {
+ iter->SeekToFirst();
+ num_seek_to_first++;
+ } else if (!iter->status().ok()) {
+ fprintf(stderr, "Iterator error: %s\n",
+ iter->status().ToString().c_str());
+ abort();
+ } else {
+ iter->Next();
+ num_next++;
+ }
+
+ thread->stats.FinishedOps(&db_, db_.db, 1, kSeek);
+ }
+ delete iter;
+ }
+
+ // Given a key K and value V, this puts (K+"0", V), (K+"1", V), (K+"2", V)
+ // in DB atomically i.e in a single batch. Also refer GetMany.
+ Status PutMany(DB* db, const WriteOptions& writeoptions, const Slice& key,
+ const Slice& value) {
+ std::string suffixes[3] = {"2", "1", "0"};
+ std::string keys[3];
+
+ WriteBatch batch(/*reserved_bytes=*/0, /*max_bytes=*/0,
+ FLAGS_write_batch_protection_bytes_per_key,
+ user_timestamp_size_);
+ Status s;
+ for (int i = 0; i < 3; i++) {
+ keys[i] = key.ToString() + suffixes[i];
+ batch.Put(keys[i], value);
+ }
+
+ std::unique_ptr<char[]> ts_guard;
+ if (user_timestamp_size_ > 0) {
+ ts_guard.reset(new char[user_timestamp_size_]);
+ Slice ts = mock_app_clock_->Allocate(ts_guard.get());
+ s = batch.UpdateTimestamps(
+ ts, [this](uint32_t) { return user_timestamp_size_; });
+ if (!s.ok()) {
+ fprintf(stderr, "assign timestamp to batch: %s\n",
+ s.ToString().c_str());
+ ErrorExit();
+ }
+ }
+
+ s = db->Write(writeoptions, &batch);
+ return s;
+ }
+
+ // Given a key K, this deletes (K+"0", V), (K+"1", V), (K+"2", V)
+ // in DB atomically i.e in a single batch. Also refer GetMany.
+ Status DeleteMany(DB* db, const WriteOptions& writeoptions,
+ const Slice& key) {
+ std::string suffixes[3] = {"1", "2", "0"};
+ std::string keys[3];
+
+ WriteBatch batch(0, 0, FLAGS_write_batch_protection_bytes_per_key,
+ user_timestamp_size_);
+ Status s;
+ for (int i = 0; i < 3; i++) {
+ keys[i] = key.ToString() + suffixes[i];
+ batch.Delete(keys[i]);
+ }
+
+ std::unique_ptr<char[]> ts_guard;
+ if (user_timestamp_size_ > 0) {
+ ts_guard.reset(new char[user_timestamp_size_]);
+ Slice ts = mock_app_clock_->Allocate(ts_guard.get());
+ s = batch.UpdateTimestamps(
+ ts, [this](uint32_t) { return user_timestamp_size_; });
+ if (!s.ok()) {
+ fprintf(stderr, "assign timestamp to batch: %s\n",
+ s.ToString().c_str());
+ ErrorExit();
+ }
+ }
+
+ s = db->Write(writeoptions, &batch);
+ return s;
+ }
+
+ // Given a key K and value V, this gets values for K+"0", K+"1" and K+"2"
+ // in the same snapshot, and verifies that all the values are identical.
+ // ASSUMES that PutMany was used to put (K, V) into the DB.
+ Status GetMany(DB* db, const Slice& key, std::string* value) {
+ std::string suffixes[3] = {"0", "1", "2"};
+ std::string keys[3];
+ Slice key_slices[3];
+ std::string values[3];
+ ReadOptions readoptionscopy = read_options_;
+
+ std::unique_ptr<char[]> ts_guard;
+ Slice ts;
+ if (user_timestamp_size_ > 0) {
+ ts_guard.reset(new char[user_timestamp_size_]);
+ ts = mock_app_clock_->Allocate(ts_guard.get());
+ readoptionscopy.timestamp = &ts;
+ }
+
+ readoptionscopy.snapshot = db->GetSnapshot();
+ Status s;
+ for (int i = 0; i < 3; i++) {
+ keys[i] = key.ToString() + suffixes[i];
+ key_slices[i] = keys[i];
+ s = db->Get(readoptionscopy, key_slices[i], value);
+ if (!s.ok() && !s.IsNotFound()) {
+ fprintf(stderr, "get error: %s\n", s.ToString().c_str());
+ values[i] = "";
+ // we continue after error rather than exiting so that we can
+ // find more errors if any
+ } else if (s.IsNotFound()) {
+ values[i] = "";
+ } else {
+ values[i] = *value;
+ }
+ }
+ db->ReleaseSnapshot(readoptionscopy.snapshot);
+
+ if ((values[0] != values[1]) || (values[1] != values[2])) {
+ fprintf(stderr, "inconsistent values for key %s: %s, %s, %s\n",
+ key.ToString().c_str(), values[0].c_str(), values[1].c_str(),
+ values[2].c_str());
+ // we continue after error rather than exiting so that we can
+ // find more errors if any
+ }
+
+ return s;
+ }
+
+ // Differs from readrandomwriterandom in the following ways:
+ // (a) Uses GetMany/PutMany to read/write key values. Refer to those funcs.
+ // (b) Does deletes as well (per FLAGS_deletepercent)
+ // (c) In order to achieve high % of 'found' during lookups, and to do
+ // multiple writes (including puts and deletes) it uses upto
+ // FLAGS_numdistinct distinct keys instead of FLAGS_num distinct keys.
+ // (d) Does not have a MultiGet option.
+ void RandomWithVerify(ThreadState* thread) {
+ RandomGenerator gen;
+ std::string value;
+ int64_t found = 0;
+ int get_weight = 0;
+ int put_weight = 0;
+ int delete_weight = 0;
+ int64_t gets_done = 0;
+ int64_t puts_done = 0;
+ int64_t deletes_done = 0;
+
+ std::unique_ptr<const char[]> key_guard;
+ Slice key = AllocateKey(&key_guard);
+
+ // the number of iterations is the larger of read_ or write_
+ for (int64_t i = 0; i < readwrites_; i++) {
+ DB* db = SelectDB(thread);
+ if (get_weight == 0 && put_weight == 0 && delete_weight == 0) {
+ // one batch completed, reinitialize for next batch
+ get_weight = FLAGS_readwritepercent;
+ delete_weight = FLAGS_deletepercent;
+ put_weight = 100 - get_weight - delete_weight;
+ }
+ GenerateKeyFromInt(thread->rand.Next() % FLAGS_numdistinct,
+ FLAGS_numdistinct, &key);
+ if (get_weight > 0) {
+ // do all the gets first
+ Status s = GetMany(db, key, &value);
+ if (!s.ok() && !s.IsNotFound()) {
+ fprintf(stderr, "getmany error: %s\n", s.ToString().c_str());
+ // we continue after error rather than exiting so that we can
+ // find more errors if any
+ } else if (!s.IsNotFound()) {
+ found++;
+ }
+ get_weight--;
+ gets_done++;
+ thread->stats.FinishedOps(&db_, db_.db, 1, kRead);
+ } else if (put_weight > 0) {
+ // then do all the corresponding number of puts
+ // for all the gets we have done earlier
+ Status s = PutMany(db, write_options_, key, gen.Generate());
+ if (!s.ok()) {
+ fprintf(stderr, "putmany error: %s\n", s.ToString().c_str());
+ exit(1);
+ }
+ put_weight--;
+ puts_done++;
+ thread->stats.FinishedOps(&db_, db_.db, 1, kWrite);
+ } else if (delete_weight > 0) {
+ Status s = DeleteMany(db, write_options_, key);
+ if (!s.ok()) {
+ fprintf(stderr, "deletemany error: %s\n", s.ToString().c_str());
+ exit(1);
+ }
+ delete_weight--;
+ deletes_done++;
+ thread->stats.FinishedOps(&db_, db_.db, 1, kDelete);
+ }
+ }
+ char msg[128];
+ snprintf(msg, sizeof(msg),
+ "( get:%" PRIu64 " put:%" PRIu64 " del:%" PRIu64 " total:%" PRIu64
+ " found:%" PRIu64 ")",
+ gets_done, puts_done, deletes_done, readwrites_, found);
+ thread->stats.AddMessage(msg);
+ }
+
+ // This is different from ReadWhileWriting because it does not use
+ // an extra thread.
+ void ReadRandomWriteRandom(ThreadState* thread) {
+ ReadOptions options = read_options_;
+ RandomGenerator gen;
+ std::string value;
+ int64_t found = 0;
+ int get_weight = 0;
+ int put_weight = 0;
+ int64_t reads_done = 0;
+ int64_t writes_done = 0;
+ Duration duration(FLAGS_duration, readwrites_);
+
+ std::unique_ptr<const char[]> key_guard;
+ Slice key = AllocateKey(&key_guard);
+
+ std::unique_ptr<char[]> ts_guard;
+ if (user_timestamp_size_ > 0) {
+ ts_guard.reset(new char[user_timestamp_size_]);
+ }
+
+ // the number of iterations is the larger of read_ or write_
+ while (!duration.Done(1)) {
+ DB* db = SelectDB(thread);
+ GenerateKeyFromInt(thread->rand.Next() % FLAGS_num, FLAGS_num, &key);
+ if (get_weight == 0 && put_weight == 0) {
+ // one batch completed, reinitialize for next batch
+ get_weight = FLAGS_readwritepercent;
+ put_weight = 100 - get_weight;
+ }
+ if (get_weight > 0) {
+ // do all the gets first
+ Slice ts;
+ if (user_timestamp_size_ > 0) {
+ ts = mock_app_clock_->GetTimestampForRead(thread->rand,
+ ts_guard.get());
+ options.timestamp = &ts;
+ }
+ Status s = db->Get(options, key, &value);
+ if (!s.ok() && !s.IsNotFound()) {
+ fprintf(stderr, "get error: %s\n", s.ToString().c_str());
+ // we continue after error rather than exiting so that we can
+ // find more errors if any
+ } else if (!s.IsNotFound()) {
+ found++;
+ }
+ get_weight--;
+ reads_done++;
+ thread->stats.FinishedOps(nullptr, db, 1, kRead);
+ } else if (put_weight > 0) {
+ // then do all the corresponding number of puts
+ // for all the gets we have done earlier
+ Status s;
+ if (user_timestamp_size_ > 0) {
+ Slice ts = mock_app_clock_->Allocate(ts_guard.get());
+ s = db->Put(write_options_, key, ts, gen.Generate());
+ } else {
+ s = db->Put(write_options_, key, gen.Generate());
+ }
+ if (!s.ok()) {
+ fprintf(stderr, "put error: %s\n", s.ToString().c_str());
+ ErrorExit();
+ }
+ put_weight--;
+ writes_done++;
+ thread->stats.FinishedOps(nullptr, db, 1, kWrite);
+ }
+ }
+ char msg[100];
+ snprintf(msg, sizeof(msg),
+ "( reads:%" PRIu64 " writes:%" PRIu64 " total:%" PRIu64
+ " found:%" PRIu64 ")",
+ reads_done, writes_done, readwrites_, found);
+ thread->stats.AddMessage(msg);
+ }
+
+ //
+ // Read-modify-write for random keys
+ void UpdateRandom(ThreadState* thread) {
+ ReadOptions options = read_options_;
+ RandomGenerator gen;
+ std::string value;
+ int64_t found = 0;
+ int64_t bytes = 0;
+ Duration duration(FLAGS_duration, readwrites_);
+
+ std::unique_ptr<const char[]> key_guard;
+ Slice key = AllocateKey(&key_guard);
+ std::unique_ptr<char[]> ts_guard;
+ if (user_timestamp_size_ > 0) {
+ ts_guard.reset(new char[user_timestamp_size_]);
+ }
+ // the number of iterations is the larger of read_ or write_
+ while (!duration.Done(1)) {
+ DB* db = SelectDB(thread);
+ GenerateKeyFromInt(thread->rand.Next() % FLAGS_num, FLAGS_num, &key);
+ Slice ts;
+ if (user_timestamp_size_ > 0) {
+ // Read with newest timestamp because we are doing rmw.
+ ts = mock_app_clock_->Allocate(ts_guard.get());
+ options.timestamp = &ts;
+ }
+
+ auto status = db->Get(options, key, &value);
+ if (status.ok()) {
+ ++found;
+ bytes += key.size() + value.size() + user_timestamp_size_;
+ } else if (!status.IsNotFound()) {
+ fprintf(stderr, "Get returned an error: %s\n",
+ status.ToString().c_str());
+ abort();
+ }
+
+ if (thread->shared->write_rate_limiter) {
+ thread->shared->write_rate_limiter->Request(
+ key.size() + value.size(), Env::IO_HIGH, nullptr /*stats*/,
+ RateLimiter::OpType::kWrite);
+ }
+
+ Slice val = gen.Generate();
+ Status s;
+ if (user_timestamp_size_ > 0) {
+ ts = mock_app_clock_->Allocate(ts_guard.get());
+ s = db->Put(write_options_, key, ts, val);
+ } else {
+ s = db->Put(write_options_, key, val);
+ }
+ if (!s.ok()) {
+ fprintf(stderr, "put error: %s\n", s.ToString().c_str());
+ exit(1);
+ }
+ bytes += key.size() + val.size() + user_timestamp_size_;
+ thread->stats.FinishedOps(nullptr, db, 1, kUpdate);
+ }
+ char msg[100];
+ snprintf(msg, sizeof(msg), "( updates:%" PRIu64 " found:%" PRIu64 ")",
+ readwrites_, found);
+ thread->stats.AddBytes(bytes);
+ thread->stats.AddMessage(msg);
+ }
+
+ // Read-XOR-write for random keys. Xors the existing value with a randomly
+ // generated value, and stores the result. Assuming A in the array of bytes
+ // representing the existing value, we generate an array B of the same size,
+ // then compute C = A^B as C[i]=A[i]^B[i], and store C
+ void XORUpdateRandom(ThreadState* thread) {
+ ReadOptions options = read_options_;
+ RandomGenerator gen;
+ std::string existing_value;
+ int64_t found = 0;
+ Duration duration(FLAGS_duration, readwrites_);
+
+ BytesXOROperator xor_operator;
+
+ std::unique_ptr<const char[]> key_guard;
+ Slice key = AllocateKey(&key_guard);
+ std::unique_ptr<char[]> ts_guard;
+ if (user_timestamp_size_ > 0) {
+ ts_guard.reset(new char[user_timestamp_size_]);
+ }
+ // the number of iterations is the larger of read_ or write_
+ while (!duration.Done(1)) {
+ DB* db = SelectDB(thread);
+ GenerateKeyFromInt(thread->rand.Next() % FLAGS_num, FLAGS_num, &key);
+ Slice ts;
+ if (user_timestamp_size_ > 0) {
+ ts = mock_app_clock_->Allocate(ts_guard.get());
+ options.timestamp = &ts;
+ }
+
+ auto status = db->Get(options, key, &existing_value);
+ if (status.ok()) {
+ ++found;
+ } else if (!status.IsNotFound()) {
+ fprintf(stderr, "Get returned an error: %s\n",
+ status.ToString().c_str());
+ exit(1);
+ }
+
+ Slice value =
+ gen.Generate(static_cast<unsigned int>(existing_value.size()));
+ std::string new_value;
+
+ if (status.ok()) {
+ Slice existing_value_slice = Slice(existing_value);
+ xor_operator.XOR(&existing_value_slice, value, &new_value);
+ } else {
+ xor_operator.XOR(nullptr, value, &new_value);
+ }
+
+ Status s;
+ if (user_timestamp_size_ > 0) {
+ ts = mock_app_clock_->Allocate(ts_guard.get());
+ s = db->Put(write_options_, key, ts, Slice(new_value));
+ } else {
+ s = db->Put(write_options_, key, Slice(new_value));
+ }
+ if (!s.ok()) {
+ fprintf(stderr, "put error: %s\n", s.ToString().c_str());
+ ErrorExit();
+ }
+ thread->stats.FinishedOps(nullptr, db, 1);
+ }
+ char msg[100];
+ snprintf(msg, sizeof(msg), "( updates:%" PRIu64 " found:%" PRIu64 ")",
+ readwrites_, found);
+ thread->stats.AddMessage(msg);
+ }
+
+ // Read-modify-write for random keys.
+ // Each operation causes the key grow by value_size (simulating an append).
+ // Generally used for benchmarking against merges of similar type
+ void AppendRandom(ThreadState* thread) {
+ ReadOptions options = read_options_;
+ RandomGenerator gen;
+ std::string value;
+ int64_t found = 0;
+ int64_t bytes = 0;
+
+ std::unique_ptr<const char[]> key_guard;
+ Slice key = AllocateKey(&key_guard);
+ std::unique_ptr<char[]> ts_guard;
+ if (user_timestamp_size_ > 0) {
+ ts_guard.reset(new char[user_timestamp_size_]);
+ }
+ // The number of iterations is the larger of read_ or write_
+ Duration duration(FLAGS_duration, readwrites_);
+ while (!duration.Done(1)) {
+ DB* db = SelectDB(thread);
+ GenerateKeyFromInt(thread->rand.Next() % FLAGS_num, FLAGS_num, &key);
+ Slice ts;
+ if (user_timestamp_size_ > 0) {
+ ts = mock_app_clock_->Allocate(ts_guard.get());
+ options.timestamp = &ts;
+ }
+
+ auto status = db->Get(options, key, &value);
+ if (status.ok()) {
+ ++found;
+ bytes += key.size() + value.size() + user_timestamp_size_;
+ } else if (!status.IsNotFound()) {
+ fprintf(stderr, "Get returned an error: %s\n",
+ status.ToString().c_str());
+ abort();
+ } else {
+ // If not existing, then just assume an empty string of data
+ value.clear();
+ }
+
+ // Update the value (by appending data)
+ Slice operand = gen.Generate();
+ if (value.size() > 0) {
+ // Use a delimiter to match the semantics for StringAppendOperator
+ value.append(1, ',');
+ }
+ value.append(operand.data(), operand.size());
+
+ Status s;
+ if (user_timestamp_size_ > 0) {
+ ts = mock_app_clock_->Allocate(ts_guard.get());
+ s = db->Put(write_options_, key, ts, value);
+ } else {
+ // Write back to the database
+ s = db->Put(write_options_, key, value);
+ }
+ if (!s.ok()) {
+ fprintf(stderr, "put error: %s\n", s.ToString().c_str());
+ ErrorExit();
+ }
+ bytes += key.size() + value.size() + user_timestamp_size_;
+ thread->stats.FinishedOps(nullptr, db, 1, kUpdate);
+ }
+
+ char msg[100];
+ snprintf(msg, sizeof(msg), "( updates:%" PRIu64 " found:%" PRIu64 ")",
+ readwrites_, found);
+ thread->stats.AddBytes(bytes);
+ thread->stats.AddMessage(msg);
+ }
+
+ // Read-modify-write for random keys (using MergeOperator)
+ // The merge operator to use should be defined by FLAGS_merge_operator
+ // Adjust FLAGS_value_size so that the keys are reasonable for this operator
+ // Assumes that the merge operator is non-null (i.e.: is well-defined)
+ //
+ // For example, use FLAGS_merge_operator="uint64add" and FLAGS_value_size=8
+ // to simulate random additions over 64-bit integers using merge.
+ //
+ // The number of merges on the same key can be controlled by adjusting
+ // FLAGS_merge_keys.
+ void MergeRandom(ThreadState* thread) {
+ RandomGenerator gen;
+ int64_t bytes = 0;
+ std::unique_ptr<const char[]> key_guard;
+ Slice key = AllocateKey(&key_guard);
+ // The number of iterations is the larger of read_ or write_
+ Duration duration(FLAGS_duration, readwrites_);
+ while (!duration.Done(1)) {
+ DBWithColumnFamilies* db_with_cfh = SelectDBWithCfh(thread);
+ int64_t key_rand = thread->rand.Next() % merge_keys_;
+ GenerateKeyFromInt(key_rand, merge_keys_, &key);
+
+ Status s;
+ Slice val = gen.Generate();
+ if (FLAGS_num_column_families > 1) {
+ s = db_with_cfh->db->Merge(write_options_,
+ db_with_cfh->GetCfh(key_rand), key, val);
+ } else {
+ s = db_with_cfh->db->Merge(
+ write_options_, db_with_cfh->db->DefaultColumnFamily(), key, val);
+ }
+
+ if (!s.ok()) {
+ fprintf(stderr, "merge error: %s\n", s.ToString().c_str());
+ exit(1);
+ }
+ bytes += key.size() + val.size();
+ thread->stats.FinishedOps(nullptr, db_with_cfh->db, 1, kMerge);
+ }
+
+ // Print some statistics
+ char msg[100];
+ snprintf(msg, sizeof(msg), "( updates:%" PRIu64 ")", readwrites_);
+ thread->stats.AddBytes(bytes);
+ thread->stats.AddMessage(msg);
+ }
+
+ // Read and merge random keys. The amount of reads and merges are controlled
+ // by adjusting FLAGS_num and FLAGS_mergereadpercent. The number of distinct
+ // keys (and thus also the number of reads and merges on the same key) can be
+ // adjusted with FLAGS_merge_keys.
+ //
+ // As with MergeRandom, the merge operator to use should be defined by
+ // FLAGS_merge_operator.
+ void ReadRandomMergeRandom(ThreadState* thread) {
+ RandomGenerator gen;
+ std::string value;
+ int64_t num_hits = 0;
+ int64_t num_gets = 0;
+ int64_t num_merges = 0;
+ size_t max_length = 0;
+
+ std::unique_ptr<const char[]> key_guard;
+ Slice key = AllocateKey(&key_guard);
+ // the number of iterations is the larger of read_ or write_
+ Duration duration(FLAGS_duration, readwrites_);
+ while (!duration.Done(1)) {
+ DB* db = SelectDB(thread);
+ GenerateKeyFromInt(thread->rand.Next() % merge_keys_, merge_keys_, &key);
+
+ bool do_merge = int(thread->rand.Next() % 100) < FLAGS_mergereadpercent;
+
+ if (do_merge) {
+ Status s = db->Merge(write_options_, key, gen.Generate());
+ if (!s.ok()) {
+ fprintf(stderr, "merge error: %s\n", s.ToString().c_str());
+ exit(1);
+ }
+ num_merges++;
+ thread->stats.FinishedOps(nullptr, db, 1, kMerge);
+ } else {
+ Status s = db->Get(read_options_, key, &value);
+ if (value.length() > max_length) max_length = value.length();
+
+ if (!s.ok() && !s.IsNotFound()) {
+ fprintf(stderr, "get error: %s\n", s.ToString().c_str());
+ // we continue after error rather than exiting so that we can
+ // find more errors if any
+ } else if (!s.IsNotFound()) {
+ num_hits++;
+ }
+ num_gets++;
+ thread->stats.FinishedOps(nullptr, db, 1, kRead);
+ }
+ }
+
+ char msg[100];
+ snprintf(msg, sizeof(msg),
+ "(reads:%" PRIu64 " merges:%" PRIu64 " total:%" PRIu64
+ " hits:%" PRIu64 " maxlength:%" ROCKSDB_PRIszt ")",
+ num_gets, num_merges, readwrites_, num_hits, max_length);
+ thread->stats.AddMessage(msg);
+ }
+
+ void WriteSeqSeekSeq(ThreadState* thread) {
+ writes_ = FLAGS_num;
+ DoWrite(thread, SEQUENTIAL);
+ // exclude writes from the ops/sec calculation
+ thread->stats.Start(thread->tid);
+
+ DB* db = SelectDB(thread);
+ ReadOptions read_opts = read_options_;
+ std::unique_ptr<char[]> ts_guard;
+ Slice ts;
+ if (user_timestamp_size_ > 0) {
+ ts_guard.reset(new char[user_timestamp_size_]);
+ ts = mock_app_clock_->GetTimestampForRead(thread->rand, ts_guard.get());
+ read_opts.timestamp = &ts;
+ }
+ std::unique_ptr<Iterator> iter(db->NewIterator(read_opts));
+
+ std::unique_ptr<const char[]> key_guard;
+ Slice key = AllocateKey(&key_guard);
+ for (int64_t i = 0; i < FLAGS_num; ++i) {
+ GenerateKeyFromInt(i, FLAGS_num, &key);
+ iter->Seek(key);
+ assert(iter->Valid() && iter->key() == key);
+ thread->stats.FinishedOps(nullptr, db, 1, kSeek);
+
+ for (int j = 0; j < FLAGS_seek_nexts && i + 1 < FLAGS_num; ++j) {
+ if (!FLAGS_reverse_iterator) {
+ iter->Next();
+ } else {
+ iter->Prev();
+ }
+ GenerateKeyFromInt(++i, FLAGS_num, &key);
+ assert(iter->Valid() && iter->key() == key);
+ thread->stats.FinishedOps(nullptr, db, 1, kSeek);
+ }
+
+ iter->Seek(key);
+ assert(iter->Valid() && iter->key() == key);
+ thread->stats.FinishedOps(nullptr, db, 1, kSeek);
+ }
+ }
+
+ bool binary_search(std::vector<int>& data, int start, int end, int key) {
+ if (data.empty()) return false;
+ if (start > end) return false;
+ int mid = start + (end - start) / 2;
+ if (mid > static_cast<int>(data.size()) - 1) return false;
+ if (data[mid] == key) {
+ return true;
+ } else if (data[mid] > key) {
+ return binary_search(data, start, mid - 1, key);
+ } else {
+ return binary_search(data, mid + 1, end, key);
+ }
+ }
+
+ // Does a bunch of merge operations for a key(key1) where the merge operand
+ // is a sorted list. Next performance comparison is done between doing a Get
+ // for key1 followed by searching for another key(key2) in the large sorted
+ // list vs calling GetMergeOperands for key1 and then searching for the key2
+ // in all the sorted sub-lists. Later case is expected to be a lot faster.
+ void GetMergeOperands(ThreadState* thread) {
+ DB* db = SelectDB(thread);
+ const int kTotalValues = 100000;
+ const int kListSize = 100;
+ std::string key = "my_key";
+ std::string value;
+
+ for (int i = 1; i < kTotalValues; i++) {
+ if (i % kListSize == 0) {
+ // Remove trailing ','
+ value.pop_back();
+ db->Merge(WriteOptions(), key, value);
+ value.clear();
+ } else {
+ value.append(std::to_string(i)).append(",");
+ }
+ }
+
+ SortList s;
+ std::vector<int> data;
+ // This value can be experimented with and it will demonstrate the
+ // perf difference between doing a Get and searching for lookup_key in the
+ // resultant large sorted list vs doing GetMergeOperands and searching
+ // for lookup_key within this resultant sorted sub-lists.
+ int lookup_key = 1;
+
+ // Get API call
+ std::cout << "--- Get API call --- \n";
+ PinnableSlice p_slice;
+ uint64_t st = FLAGS_env->NowNanos();
+ db->Get(ReadOptions(), db->DefaultColumnFamily(), key, &p_slice);
+ s.MakeVector(data, p_slice);
+ bool found =
+ binary_search(data, 0, static_cast<int>(data.size() - 1), lookup_key);
+ std::cout << "Found key? " << std::to_string(found) << "\n";
+ uint64_t sp = FLAGS_env->NowNanos();
+ std::cout << "Get: " << (sp - st) / 1000000000.0 << " seconds\n";
+ std::string* dat_ = p_slice.GetSelf();
+ std::cout << "Sample data from Get API call: " << dat_->substr(0, 10)
+ << "\n";
+ data.clear();
+
+ // GetMergeOperands API call
+ std::cout << "--- GetMergeOperands API --- \n";
+ std::vector<PinnableSlice> a_slice((kTotalValues / kListSize) + 1);
+ st = FLAGS_env->NowNanos();
+ int number_of_operands = 0;
+ GetMergeOperandsOptions get_merge_operands_options;
+ get_merge_operands_options.expected_max_number_of_operands =
+ (kTotalValues / 100) + 1;
+ db->GetMergeOperands(ReadOptions(), db->DefaultColumnFamily(), key,
+ a_slice.data(), &get_merge_operands_options,
+ &number_of_operands);
+ for (PinnableSlice& psl : a_slice) {
+ s.MakeVector(data, psl);
+ found =
+ binary_search(data, 0, static_cast<int>(data.size() - 1), lookup_key);
+ data.clear();
+ if (found) break;
+ }
+ std::cout << "Found key? " << std::to_string(found) << "\n";
+ sp = FLAGS_env->NowNanos();
+ std::cout << "Get Merge operands: " << (sp - st) / 1000000000.0
+ << " seconds \n";
+ int to_print = 0;
+ std::cout << "Sample data from GetMergeOperands API call: ";
+ for (PinnableSlice& psl : a_slice) {
+ std::cout << "List: " << to_print << " : " << *psl.GetSelf() << "\n";
+ if (to_print++ > 2) break;
+ }
+ }
+
+#ifndef ROCKSDB_LITE
+ void VerifyChecksum(ThreadState* thread) {
+ DB* db = SelectDB(thread);
+ ReadOptions ro;
+ ro.adaptive_readahead = FLAGS_adaptive_readahead;
+ ro.async_io = FLAGS_async_io;
+ ro.rate_limiter_priority =
+ FLAGS_rate_limit_user_ops ? Env::IO_USER : Env::IO_TOTAL;
+ ro.readahead_size = FLAGS_readahead_size;
+ Status s = db->VerifyChecksum(ro);
+ if (!s.ok()) {
+ fprintf(stderr, "VerifyChecksum() failed: %s\n", s.ToString().c_str());
+ exit(1);
+ }
+ }
+
+ void VerifyFileChecksums(ThreadState* thread) {
+ DB* db = SelectDB(thread);
+ ReadOptions ro;
+ ro.adaptive_readahead = FLAGS_adaptive_readahead;
+ ro.async_io = FLAGS_async_io;
+ ro.rate_limiter_priority =
+ FLAGS_rate_limit_user_ops ? Env::IO_USER : Env::IO_TOTAL;
+ ro.readahead_size = FLAGS_readahead_size;
+ Status s = db->VerifyFileChecksums(ro);
+ if (!s.ok()) {
+ fprintf(stderr, "VerifyFileChecksums() failed: %s\n",
+ s.ToString().c_str());
+ exit(1);
+ }
+ }
+
+ // This benchmark stress tests Transactions. For a given --duration (or
+ // total number of --writes, a Transaction will perform a read-modify-write
+ // to increment the value of a key in each of N(--transaction-sets) sets of
+ // keys (where each set has --num keys). If --threads is set, this will be
+ // done in parallel.
+ //
+ // To test transactions, use --transaction_db=true. Not setting this
+ // parameter
+ // will run the same benchmark without transactions.
+ //
+ // RandomTransactionVerify() will then validate the correctness of the results
+ // by checking if the sum of all keys in each set is the same.
+ void RandomTransaction(ThreadState* thread) {
+ Duration duration(FLAGS_duration, readwrites_);
+ uint16_t num_prefix_ranges = static_cast<uint16_t>(FLAGS_transaction_sets);
+ uint64_t transactions_done = 0;
+
+ if (num_prefix_ranges == 0 || num_prefix_ranges > 9999) {
+ fprintf(stderr, "invalid value for transaction_sets\n");
+ abort();
+ }
+
+ TransactionOptions txn_options;
+ txn_options.lock_timeout = FLAGS_transaction_lock_timeout;
+ txn_options.set_snapshot = FLAGS_transaction_set_snapshot;
+
+ RandomTransactionInserter inserter(&thread->rand, write_options_,
+ read_options_, FLAGS_num,
+ num_prefix_ranges);
+
+ if (FLAGS_num_multi_db > 1) {
+ fprintf(stderr,
+ "Cannot run RandomTransaction benchmark with "
+ "FLAGS_multi_db > 1.");
+ abort();
+ }
+
+ while (!duration.Done(1)) {
+ bool success;
+
+ // RandomTransactionInserter will attempt to insert a key for each
+ // # of FLAGS_transaction_sets
+ if (FLAGS_optimistic_transaction_db) {
+ success = inserter.OptimisticTransactionDBInsert(db_.opt_txn_db);
+ } else if (FLAGS_transaction_db) {
+ TransactionDB* txn_db = reinterpret_cast<TransactionDB*>(db_.db);
+ success = inserter.TransactionDBInsert(txn_db, txn_options);
+ } else {
+ success = inserter.DBInsert(db_.db);
+ }
+
+ if (!success) {
+ fprintf(stderr, "Unexpected error: %s\n",
+ inserter.GetLastStatus().ToString().c_str());
+ abort();
+ }
+
+ thread->stats.FinishedOps(nullptr, db_.db, 1, kOthers);
+ transactions_done++;
+ }
+
+ char msg[100];
+ if (FLAGS_optimistic_transaction_db || FLAGS_transaction_db) {
+ snprintf(msg, sizeof(msg),
+ "( transactions:%" PRIu64 " aborts:%" PRIu64 ")",
+ transactions_done, inserter.GetFailureCount());
+ } else {
+ snprintf(msg, sizeof(msg), "( batches:%" PRIu64 " )", transactions_done);
+ }
+ thread->stats.AddMessage(msg);
+ thread->stats.AddBytes(static_cast<int64_t>(inserter.GetBytesInserted()));
+ }
+
+ // Verifies consistency of data after RandomTransaction() has been run.
+ // Since each iteration of RandomTransaction() incremented a key in each set
+ // by the same value, the sum of the keys in each set should be the same.
+ void RandomTransactionVerify() {
+ if (!FLAGS_transaction_db && !FLAGS_optimistic_transaction_db) {
+ // transactions not used, nothing to verify.
+ return;
+ }
+
+ Status s = RandomTransactionInserter::Verify(
+ db_.db, static_cast<uint16_t>(FLAGS_transaction_sets));
+
+ if (s.ok()) {
+ fprintf(stdout, "RandomTransactionVerify Success.\n");
+ } else {
+ fprintf(stdout, "RandomTransactionVerify FAILED!!\n");
+ }
+ }
+#endif // ROCKSDB_LITE
+
+ // Writes and deletes random keys without overwriting keys.
+ //
+ // This benchmark is intended to partially replicate the behavior of MyRocks
+ // secondary indices: All data is stored in keys and updates happen by
+ // deleting the old version of the key and inserting the new version.
+ void RandomReplaceKeys(ThreadState* thread) {
+ std::unique_ptr<const char[]> key_guard;
+ Slice key = AllocateKey(&key_guard);
+ std::unique_ptr<char[]> ts_guard;
+ if (user_timestamp_size_ > 0) {
+ ts_guard.reset(new char[user_timestamp_size_]);
+ }
+ std::vector<uint32_t> counters(FLAGS_numdistinct, 0);
+ size_t max_counter = 50;
+ RandomGenerator gen;
+
+ Status s;
+ DB* db = SelectDB(thread);
+ for (int64_t i = 0; i < FLAGS_numdistinct; i++) {
+ GenerateKeyFromInt(i * max_counter, FLAGS_num, &key);
+ if (user_timestamp_size_ > 0) {
+ Slice ts = mock_app_clock_->Allocate(ts_guard.get());
+ s = db->Put(write_options_, key, ts, gen.Generate());
+ } else {
+ s = db->Put(write_options_, key, gen.Generate());
+ }
+ if (!s.ok()) {
+ fprintf(stderr, "Operation failed: %s\n", s.ToString().c_str());
+ exit(1);
+ }
+ }
+
+ db->GetSnapshot();
+
+ std::default_random_engine generator;
+ std::normal_distribution<double> distribution(FLAGS_numdistinct / 2.0,
+ FLAGS_stddev);
+ Duration duration(FLAGS_duration, FLAGS_num);
+ while (!duration.Done(1)) {
+ int64_t rnd_id = static_cast<int64_t>(distribution(generator));
+ int64_t key_id = std::max(std::min(FLAGS_numdistinct - 1, rnd_id),
+ static_cast<int64_t>(0));
+ GenerateKeyFromInt(key_id * max_counter + counters[key_id], FLAGS_num,
+ &key);
+ if (user_timestamp_size_ > 0) {
+ Slice ts = mock_app_clock_->Allocate(ts_guard.get());
+ s = FLAGS_use_single_deletes ? db->SingleDelete(write_options_, key, ts)
+ : db->Delete(write_options_, key, ts);
+ } else {
+ s = FLAGS_use_single_deletes ? db->SingleDelete(write_options_, key)
+ : db->Delete(write_options_, key);
+ }
+ if (s.ok()) {
+ counters[key_id] = (counters[key_id] + 1) % max_counter;
+ GenerateKeyFromInt(key_id * max_counter + counters[key_id], FLAGS_num,
+ &key);
+ if (user_timestamp_size_ > 0) {
+ Slice ts = mock_app_clock_->Allocate(ts_guard.get());
+ s = db->Put(write_options_, key, ts, Slice());
+ } else {
+ s = db->Put(write_options_, key, Slice());
+ }
+ }
+
+ if (!s.ok()) {
+ fprintf(stderr, "Operation failed: %s\n", s.ToString().c_str());
+ exit(1);
+ }
+
+ thread->stats.FinishedOps(nullptr, db, 1, kOthers);
+ }
+
+ char msg[200];
+ snprintf(msg, sizeof(msg),
+ "use single deletes: %d, "
+ "standard deviation: %lf\n",
+ FLAGS_use_single_deletes, FLAGS_stddev);
+ thread->stats.AddMessage(msg);
+ }
+
+ void TimeSeriesReadOrDelete(ThreadState* thread, bool do_deletion) {
+ int64_t read = 0;
+ int64_t found = 0;
+ int64_t bytes = 0;
+
+ Iterator* iter = nullptr;
+ // Only work on single database
+ assert(db_.db != nullptr);
+ iter = db_.db->NewIterator(read_options_);
+
+ std::unique_ptr<const char[]> key_guard;
+ Slice key = AllocateKey(&key_guard);
+
+ char value_buffer[256];
+ while (true) {
+ {
+ MutexLock l(&thread->shared->mu);
+ if (thread->shared->num_done >= 1) {
+ // Write thread have finished
+ break;
+ }
+ }
+ if (!FLAGS_use_tailing_iterator) {
+ delete iter;
+ iter = db_.db->NewIterator(read_options_);
+ }
+ // Pick a Iterator to use
+
+ int64_t key_id = thread->rand.Next() % FLAGS_key_id_range;
+ GenerateKeyFromInt(key_id, FLAGS_num, &key);
+ // Reset last 8 bytes to 0
+ char* start = const_cast<char*>(key.data());
+ start += key.size() - 8;
+ memset(start, 0, 8);
+ ++read;
+
+ bool key_found = false;
+ // Seek the prefix
+ for (iter->Seek(key); iter->Valid() && iter->key().starts_with(key);
+ iter->Next()) {
+ key_found = true;
+ // Copy out iterator's value to make sure we read them.
+ if (do_deletion) {
+ bytes += iter->key().size();
+ if (KeyExpired(timestamp_emulator_.get(), iter->key())) {
+ thread->stats.FinishedOps(&db_, db_.db, 1, kDelete);
+ db_.db->Delete(write_options_, iter->key());
+ } else {
+ break;
+ }
+ } else {
+ bytes += iter->key().size() + iter->value().size();
+ thread->stats.FinishedOps(&db_, db_.db, 1, kRead);
+ Slice value = iter->value();
+ memcpy(value_buffer, value.data(),
+ std::min(value.size(), sizeof(value_buffer)));
+
+ assert(iter->status().ok());
+ }
+ }
+ found += key_found;
+
+ if (thread->shared->read_rate_limiter.get() != nullptr) {
+ thread->shared->read_rate_limiter->Request(
+ 1, Env::IO_HIGH, nullptr /* stats */, RateLimiter::OpType::kRead);
+ }
+ }
+ delete iter;
+
+ char msg[100];
+ snprintf(msg, sizeof(msg), "(%" PRIu64 " of %" PRIu64 " found)", found,
+ read);
+ thread->stats.AddBytes(bytes);
+ thread->stats.AddMessage(msg);
+ }
+
+ void TimeSeriesWrite(ThreadState* thread) {
+ // Special thread that keeps writing until other threads are done.
+ RandomGenerator gen;
+ int64_t bytes = 0;
+
+ // Don't merge stats from this thread with the readers.
+ thread->stats.SetExcludeFromMerge();
+
+ std::unique_ptr<RateLimiter> write_rate_limiter;
+ if (FLAGS_benchmark_write_rate_limit > 0) {
+ write_rate_limiter.reset(
+ NewGenericRateLimiter(FLAGS_benchmark_write_rate_limit));
+ }
+
+ std::unique_ptr<const char[]> key_guard;
+ Slice key = AllocateKey(&key_guard);
+
+ Duration duration(FLAGS_duration, writes_);
+ while (!duration.Done(1)) {
+ DB* db = SelectDB(thread);
+
+ uint64_t key_id = thread->rand.Next() % FLAGS_key_id_range;
+ // Write key id
+ GenerateKeyFromInt(key_id, FLAGS_num, &key);
+ // Write timestamp
+
+ char* start = const_cast<char*>(key.data());
+ char* pos = start + 8;
+ int bytes_to_fill =
+ std::min(key_size_ - static_cast<int>(pos - start), 8);
+ uint64_t timestamp_value = timestamp_emulator_->Get();
+ if (port::kLittleEndian) {
+ for (int i = 0; i < bytes_to_fill; ++i) {
+ pos[i] = (timestamp_value >> ((bytes_to_fill - i - 1) << 3)) & 0xFF;
+ }
+ } else {
+ memcpy(pos, static_cast<void*>(&timestamp_value), bytes_to_fill);
+ }
+
+ timestamp_emulator_->Inc();
+
+ Status s;
+ Slice val = gen.Generate();
+ s = db->Put(write_options_, key, val);
+
+ if (!s.ok()) {
+ fprintf(stderr, "put error: %s\n", s.ToString().c_str());
+ ErrorExit();
+ }
+ bytes = key.size() + val.size();
+ thread->stats.FinishedOps(&db_, db_.db, 1, kWrite);
+ thread->stats.AddBytes(bytes);
+
+ if (FLAGS_benchmark_write_rate_limit > 0) {
+ write_rate_limiter->Request(key.size() + val.size(), Env::IO_HIGH,
+ nullptr /* stats */,
+ RateLimiter::OpType::kWrite);
+ }
+ }
+ }
+
+ void TimeSeries(ThreadState* thread) {
+ if (thread->tid > 0) {
+ bool do_deletion = FLAGS_expire_style == "delete" &&
+ thread->tid <= FLAGS_num_deletion_threads;
+ TimeSeriesReadOrDelete(thread, do_deletion);
+ } else {
+ TimeSeriesWrite(thread);
+ thread->stats.Stop();
+ thread->stats.Report("timeseries write");
+ }
+ }
+
+ void Compact(ThreadState* thread) {
+ DB* db = SelectDB(thread);
+ CompactRangeOptions cro;
+ cro.bottommost_level_compaction =
+ BottommostLevelCompaction::kForceOptimized;
+ db->CompactRange(cro, nullptr, nullptr);
+ }
+
+ void CompactAll() {
+ if (db_.db != nullptr) {
+ db_.db->CompactRange(CompactRangeOptions(), nullptr, nullptr);
+ }
+ for (const auto& db_with_cfh : multi_dbs_) {
+ db_with_cfh.db->CompactRange(CompactRangeOptions(), nullptr, nullptr);
+ }
+ }
+
+#ifndef ROCKSDB_LITE
+ void WaitForCompactionHelper(DBWithColumnFamilies& db) {
+ // This is an imperfect way of waiting for compaction. The loop and sleep
+ // is done because a thread that finishes a compaction job should get a
+ // chance to pickup a new compaction job.
+
+ std::vector<std::string> keys = {DB::Properties::kMemTableFlushPending,
+ DB::Properties::kNumRunningFlushes,
+ DB::Properties::kCompactionPending,
+ DB::Properties::kNumRunningCompactions};
+
+ fprintf(stdout, "waitforcompaction(%s): started\n",
+ db.db->GetName().c_str());
+
+ while (true) {
+ bool retry = false;
+
+ for (const auto& k : keys) {
+ uint64_t v;
+ if (!db.db->GetIntProperty(k, &v)) {
+ fprintf(stderr, "waitforcompaction(%s): GetIntProperty(%s) failed\n",
+ db.db->GetName().c_str(), k.c_str());
+ exit(1);
+ } else if (v > 0) {
+ fprintf(stdout,
+ "waitforcompaction(%s): active(%s). Sleep 10 seconds\n",
+ db.db->GetName().c_str(), k.c_str());
+ FLAGS_env->SleepForMicroseconds(10 * 1000000);
+ retry = true;
+ break;
+ }
+ }
+
+ if (!retry) {
+ fprintf(stdout, "waitforcompaction(%s): finished\n",
+ db.db->GetName().c_str());
+ return;
+ }
+ }
+ }
+
+ void WaitForCompaction() {
+ // Give background threads a chance to wake
+ FLAGS_env->SleepForMicroseconds(5 * 1000000);
+
+ // I am skeptical that this check race free. I hope that checking twice
+ // reduces the chance.
+ if (db_.db != nullptr) {
+ WaitForCompactionHelper(db_);
+ WaitForCompactionHelper(db_);
+ } else {
+ for (auto& db_with_cfh : multi_dbs_) {
+ WaitForCompactionHelper(db_with_cfh);
+ WaitForCompactionHelper(db_with_cfh);
+ }
+ }
+ }
+
+ bool CompactLevelHelper(DBWithColumnFamilies& db_with_cfh, int from_level) {
+ std::vector<LiveFileMetaData> files;
+ db_with_cfh.db->GetLiveFilesMetaData(&files);
+
+ assert(from_level == 0 || from_level == 1);
+
+ int real_from_level = from_level;
+ if (real_from_level > 0) {
+ // With dynamic leveled compaction the first level with data beyond L0
+ // might not be L1.
+ real_from_level = std::numeric_limits<int>::max();
+
+ for (auto& f : files) {
+ if (f.level > 0 && f.level < real_from_level) real_from_level = f.level;
+ }
+
+ if (real_from_level == std::numeric_limits<int>::max()) {
+ fprintf(stdout, "compact%d found 0 files to compact\n", from_level);
+ return true;
+ }
+ }
+
+ // The goal is to compact from from_level to the level that follows it,
+ // and with dynamic leveled compaction the next level might not be
+ // real_from_level+1
+ int next_level = std::numeric_limits<int>::max();
+
+ std::vector<std::string> files_to_compact;
+ for (auto& f : files) {
+ if (f.level == real_from_level)
+ files_to_compact.push_back(f.name);
+ else if (f.level > real_from_level && f.level < next_level)
+ next_level = f.level;
+ }
+
+ if (files_to_compact.empty()) {
+ fprintf(stdout, "compact%d found 0 files to compact\n", from_level);
+ return true;
+ } else if (next_level == std::numeric_limits<int>::max()) {
+ // There is no data beyond real_from_level. So we are done.
+ fprintf(stdout, "compact%d found no data beyond L%d\n", from_level,
+ real_from_level);
+ return true;
+ }
+
+ fprintf(stdout, "compact%d found %d files to compact from L%d to L%d\n",
+ from_level, static_cast<int>(files_to_compact.size()),
+ real_from_level, next_level);
+
+ ROCKSDB_NAMESPACE::CompactionOptions options;
+ // Lets RocksDB use the configured compression for this level
+ options.compression = ROCKSDB_NAMESPACE::kDisableCompressionOption;
+
+ ROCKSDB_NAMESPACE::ColumnFamilyDescriptor cfDesc;
+ db_with_cfh.db->DefaultColumnFamily()->GetDescriptor(&cfDesc);
+ options.output_file_size_limit = cfDesc.options.target_file_size_base;
+
+ Status status =
+ db_with_cfh.db->CompactFiles(options, files_to_compact, next_level);
+ if (!status.ok()) {
+ // This can fail for valid reasons including the operation was aborted
+ // or a filename is invalid because background compaction removed it.
+ // Having read the current cases for which an error is raised I prefer
+ // not to figure out whether an exception should be thrown here.
+ fprintf(stderr, "compact%d CompactFiles failed: %s\n", from_level,
+ status.ToString().c_str());
+ return false;
+ }
+ return true;
+ }
+
+ void CompactLevel(int from_level) {
+ if (db_.db != nullptr) {
+ while (!CompactLevelHelper(db_, from_level)) WaitForCompaction();
+ }
+ for (auto& db_with_cfh : multi_dbs_) {
+ while (!CompactLevelHelper(db_with_cfh, from_level)) WaitForCompaction();
+ }
+ }
+#endif
+
+ void Flush() {
+ FlushOptions flush_opt;
+ flush_opt.wait = true;
+
+ if (db_.db != nullptr) {
+ Status s;
+ if (FLAGS_num_column_families > 1) {
+ s = db_.db->Flush(flush_opt, db_.cfh);
+ } else {
+ s = db_.db->Flush(flush_opt, db_.db->DefaultColumnFamily());
+ }
+
+ if (!s.ok()) {
+ fprintf(stderr, "Flush failed: %s\n", s.ToString().c_str());
+ exit(1);
+ }
+ } else {
+ for (const auto& db_with_cfh : multi_dbs_) {
+ Status s;
+ if (FLAGS_num_column_families > 1) {
+ s = db_with_cfh.db->Flush(flush_opt, db_with_cfh.cfh);
+ } else {
+ s = db_with_cfh.db->Flush(flush_opt,
+ db_with_cfh.db->DefaultColumnFamily());
+ }
+
+ if (!s.ok()) {
+ fprintf(stderr, "Flush failed: %s\n", s.ToString().c_str());
+ exit(1);
+ }
+ }
+ }
+ fprintf(stdout, "flush memtable\n");
+ }
+
+ void ResetStats() {
+ if (db_.db != nullptr) {
+ db_.db->ResetStats();
+ }
+ for (const auto& db_with_cfh : multi_dbs_) {
+ db_with_cfh.db->ResetStats();
+ }
+ }
+
+ void PrintStatsHistory() {
+ if (db_.db != nullptr) {
+ PrintStatsHistoryImpl(db_.db, false);
+ }
+ for (const auto& db_with_cfh : multi_dbs_) {
+ PrintStatsHistoryImpl(db_with_cfh.db, true);
+ }
+ }
+
+ void PrintStatsHistoryImpl(DB* db, bool print_header) {
+ if (print_header) {
+ fprintf(stdout, "\n==== DB: %s ===\n", db->GetName().c_str());
+ }
+
+ std::unique_ptr<StatsHistoryIterator> shi;
+ Status s =
+ db->GetStatsHistory(0, std::numeric_limits<uint64_t>::max(), &shi);
+ if (!s.ok()) {
+ fprintf(stdout, "%s\n", s.ToString().c_str());
+ return;
+ }
+ assert(shi);
+ while (shi->Valid()) {
+ uint64_t stats_time = shi->GetStatsTime();
+ fprintf(stdout, "------ %s ------\n",
+ TimeToHumanString(static_cast<int>(stats_time)).c_str());
+ for (auto& entry : shi->GetStatsMap()) {
+ fprintf(stdout, " %" PRIu64 " %s %" PRIu64 "\n", stats_time,
+ entry.first.c_str(), entry.second);
+ }
+ shi->Next();
+ }
+ }
+
+ void PrintStats(const char* key) {
+ if (db_.db != nullptr) {
+ PrintStats(db_.db, key, false);
+ }
+ for (const auto& db_with_cfh : multi_dbs_) {
+ PrintStats(db_with_cfh.db, key, true);
+ }
+ }
+
+ void PrintStats(DB* db, const char* key, bool print_header = false) {
+ if (print_header) {
+ fprintf(stdout, "\n==== DB: %s ===\n", db->GetName().c_str());
+ }
+ std::string stats;
+ if (!db->GetProperty(key, &stats)) {
+ stats = "(failed)";
+ }
+ fprintf(stdout, "\n%s\n", stats.c_str());
+ }
+
+ void PrintStats(const std::vector<std::string>& keys) {
+ if (db_.db != nullptr) {
+ PrintStats(db_.db, keys);
+ }
+ for (const auto& db_with_cfh : multi_dbs_) {
+ PrintStats(db_with_cfh.db, keys, true);
+ }
+ }
+
+ void PrintStats(DB* db, const std::vector<std::string>& keys,
+ bool print_header = false) {
+ if (print_header) {
+ fprintf(stdout, "\n==== DB: %s ===\n", db->GetName().c_str());
+ }
+
+ for (const auto& key : keys) {
+ std::string stats;
+ if (!db->GetProperty(key, &stats)) {
+ stats = "(failed)";
+ }
+ fprintf(stdout, "%s: %s\n", key.c_str(), stats.c_str());
+ }
+ }
+
+#ifndef ROCKSDB_LITE
+
+ void Replay(ThreadState* thread) {
+ if (db_.db != nullptr) {
+ Replay(thread, &db_);
+ }
+ }
+
+ void Replay(ThreadState* /*thread*/, DBWithColumnFamilies* db_with_cfh) {
+ Status s;
+ std::unique_ptr<TraceReader> trace_reader;
+ s = NewFileTraceReader(FLAGS_env, EnvOptions(), FLAGS_trace_file,
+ &trace_reader);
+ if (!s.ok()) {
+ fprintf(
+ stderr,
+ "Encountered an error creating a TraceReader from the trace file. "
+ "Error: %s\n",
+ s.ToString().c_str());
+ exit(1);
+ }
+ std::unique_ptr<Replayer> replayer;
+ s = db_with_cfh->db->NewDefaultReplayer(db_with_cfh->cfh,
+ std::move(trace_reader), &replayer);
+ if (!s.ok()) {
+ fprintf(stderr,
+ "Encountered an error creating a default Replayer. "
+ "Error: %s\n",
+ s.ToString().c_str());
+ exit(1);
+ }
+ s = replayer->Prepare();
+ if (!s.ok()) {
+ fprintf(stderr, "Prepare for replay failed. Error: %s\n",
+ s.ToString().c_str());
+ }
+ s = replayer->Replay(
+ ReplayOptions(static_cast<uint32_t>(FLAGS_trace_replay_threads),
+ FLAGS_trace_replay_fast_forward),
+ nullptr);
+ replayer.reset();
+ if (s.ok()) {
+ fprintf(stdout, "Replay completed from trace_file: %s\n",
+ FLAGS_trace_file.c_str());
+ } else {
+ fprintf(stderr, "Replay failed. Error: %s\n", s.ToString().c_str());
+ }
+ }
+
+ void Backup(ThreadState* thread) {
+ DB* db = SelectDB(thread);
+ std::unique_ptr<BackupEngineOptions> engine_options(
+ new BackupEngineOptions(FLAGS_backup_dir));
+ Status s;
+ BackupEngine* backup_engine;
+ if (FLAGS_backup_rate_limit > 0) {
+ engine_options->backup_rate_limiter.reset(NewGenericRateLimiter(
+ FLAGS_backup_rate_limit, 100000 /* refill_period_us */,
+ 10 /* fairness */, RateLimiter::Mode::kAllIo));
+ }
+ // Build new backup of the entire DB
+ engine_options->destroy_old_data = true;
+ s = BackupEngine::Open(FLAGS_env, *engine_options, &backup_engine);
+ assert(s.ok());
+ s = backup_engine->CreateNewBackup(db);
+ assert(s.ok());
+ std::vector<BackupInfo> backup_info;
+ backup_engine->GetBackupInfo(&backup_info);
+ // Verify that a new backup is created
+ assert(backup_info.size() == 1);
+ }
+
+ void Restore(ThreadState* /* thread */) {
+ std::unique_ptr<BackupEngineOptions> engine_options(
+ new BackupEngineOptions(FLAGS_backup_dir));
+ if (FLAGS_restore_rate_limit > 0) {
+ engine_options->restore_rate_limiter.reset(NewGenericRateLimiter(
+ FLAGS_restore_rate_limit, 100000 /* refill_period_us */,
+ 10 /* fairness */, RateLimiter::Mode::kAllIo));
+ }
+ BackupEngineReadOnly* backup_engine;
+ Status s =
+ BackupEngineReadOnly::Open(FLAGS_env, *engine_options, &backup_engine);
+ assert(s.ok());
+ s = backup_engine->RestoreDBFromLatestBackup(FLAGS_restore_dir,
+ FLAGS_restore_dir);
+ assert(s.ok());
+ delete backup_engine;
+ }
+
+#endif // ROCKSDB_LITE
+};
+
+int db_bench_tool(int argc, char** argv) {
+ ROCKSDB_NAMESPACE::port::InstallStackTraceHandler();
+ ConfigOptions config_options;
+ static bool initialized = false;
+ if (!initialized) {
+ SetUsageMessage(std::string("\nUSAGE:\n") + std::string(argv[0]) +
+ " [OPTIONS]...");
+ SetVersionString(GetRocksVersionAsString(true));
+ initialized = true;
+ }
+ ParseCommandLineFlags(&argc, &argv, true);
+ FLAGS_compaction_style_e =
+ (ROCKSDB_NAMESPACE::CompactionStyle)FLAGS_compaction_style;
+#ifndef ROCKSDB_LITE
+ if (FLAGS_statistics && !FLAGS_statistics_string.empty()) {
+ fprintf(stderr,
+ "Cannot provide both --statistics and --statistics_string.\n");
+ exit(1);
+ }
+ if (!FLAGS_statistics_string.empty()) {
+ Status s = Statistics::CreateFromString(config_options,
+ FLAGS_statistics_string, &dbstats);
+ if (dbstats == nullptr) {
+ fprintf(stderr,
+ "No Statistics registered matching string: %s status=%s\n",
+ FLAGS_statistics_string.c_str(), s.ToString().c_str());
+ exit(1);
+ }
+ }
+#endif // ROCKSDB_LITE
+ if (FLAGS_statistics) {
+ dbstats = ROCKSDB_NAMESPACE::CreateDBStatistics();
+ }
+ if (dbstats) {
+ dbstats->set_stats_level(static_cast<StatsLevel>(FLAGS_stats_level));
+ }
+ FLAGS_compaction_pri_e =
+ (ROCKSDB_NAMESPACE::CompactionPri)FLAGS_compaction_pri;
+
+ std::vector<std::string> fanout = ROCKSDB_NAMESPACE::StringSplit(
+ FLAGS_max_bytes_for_level_multiplier_additional, ',');
+ for (size_t j = 0; j < fanout.size(); j++) {
+ FLAGS_max_bytes_for_level_multiplier_additional_v.push_back(
+#ifndef CYGWIN
+ std::stoi(fanout[j]));
+#else
+ stoi(fanout[j]));
+#endif
+ }
+
+ FLAGS_compression_type_e =
+ StringToCompressionType(FLAGS_compression_type.c_str());
+
+ FLAGS_wal_compression_e =
+ StringToCompressionType(FLAGS_wal_compression.c_str());
+
+ FLAGS_compressed_secondary_cache_compression_type_e = StringToCompressionType(
+ FLAGS_compressed_secondary_cache_compression_type.c_str());
+
+#ifndef ROCKSDB_LITE
+ // Stacked BlobDB
+ FLAGS_blob_db_compression_type_e =
+ StringToCompressionType(FLAGS_blob_db_compression_type.c_str());
+
+ int env_opts = !FLAGS_env_uri.empty() + !FLAGS_fs_uri.empty();
+ if (env_opts > 1) {
+ fprintf(stderr, "Error: --env_uri and --fs_uri are mutually exclusive\n");
+ exit(1);
+ }
+
+ if (env_opts == 1) {
+ Status s = Env::CreateFromUri(config_options, FLAGS_env_uri, FLAGS_fs_uri,
+ &FLAGS_env, &env_guard);
+ if (!s.ok()) {
+ fprintf(stderr, "Failed creating env: %s\n", s.ToString().c_str());
+ exit(1);
+ }
+ } else if (FLAGS_simulate_hdd || FLAGS_simulate_hybrid_fs_file != "") {
+ //**TODO: Make the simulate fs something that can be loaded
+ // from the ObjectRegistry...
+ static std::shared_ptr<ROCKSDB_NAMESPACE::Env> composite_env =
+ NewCompositeEnv(std::make_shared<SimulatedHybridFileSystem>(
+ FileSystem::Default(), FLAGS_simulate_hybrid_fs_file,
+ /*throughput_multiplier=*/
+ int{FLAGS_simulate_hybrid_hdd_multipliers},
+ /*is_full_fs_warm=*/FLAGS_simulate_hdd));
+ FLAGS_env = composite_env.get();
+ }
+
+ // Let -readonly imply -use_existing_db
+ FLAGS_use_existing_db |= FLAGS_readonly;
+#endif // ROCKSDB_LITE
+
+ if (FLAGS_build_info) {
+ std::string build_info;
+ std::cout << GetRocksBuildInfoAsString(build_info, true) << std::endl;
+ // Similar to --version, nothing else will be done when this flag is set
+ exit(0);
+ }
+
+ if (!FLAGS_seed) {
+ uint64_t now = FLAGS_env->GetSystemClock()->NowMicros();
+ seed_base = static_cast<int64_t>(now);
+ fprintf(stdout, "Set seed to %" PRIu64 " because --seed was 0\n",
+ seed_base);
+ } else {
+ seed_base = FLAGS_seed;
+ }
+
+ if (FLAGS_use_existing_keys && !FLAGS_use_existing_db) {
+ fprintf(stderr,
+ "`-use_existing_db` must be true for `-use_existing_keys` to be "
+ "settable\n");
+ exit(1);
+ }
+
+ if (!strcasecmp(FLAGS_compaction_fadvice.c_str(), "NONE"))
+ FLAGS_compaction_fadvice_e = ROCKSDB_NAMESPACE::Options::NONE;
+ else if (!strcasecmp(FLAGS_compaction_fadvice.c_str(), "NORMAL"))
+ FLAGS_compaction_fadvice_e = ROCKSDB_NAMESPACE::Options::NORMAL;
+ else if (!strcasecmp(FLAGS_compaction_fadvice.c_str(), "SEQUENTIAL"))
+ FLAGS_compaction_fadvice_e = ROCKSDB_NAMESPACE::Options::SEQUENTIAL;
+ else if (!strcasecmp(FLAGS_compaction_fadvice.c_str(), "WILLNEED"))
+ FLAGS_compaction_fadvice_e = ROCKSDB_NAMESPACE::Options::WILLNEED;
+ else {
+ fprintf(stdout, "Unknown compaction fadvice:%s\n",
+ FLAGS_compaction_fadvice.c_str());
+ exit(1);
+ }
+
+ FLAGS_value_size_distribution_type_e =
+ StringToDistributionType(FLAGS_value_size_distribution_type.c_str());
+
+ // Note options sanitization may increase thread pool sizes according to
+ // max_background_flushes/max_background_compactions/max_background_jobs
+ FLAGS_env->SetBackgroundThreads(FLAGS_num_high_pri_threads,
+ ROCKSDB_NAMESPACE::Env::Priority::HIGH);
+ FLAGS_env->SetBackgroundThreads(FLAGS_num_bottom_pri_threads,
+ ROCKSDB_NAMESPACE::Env::Priority::BOTTOM);
+ FLAGS_env->SetBackgroundThreads(FLAGS_num_low_pri_threads,
+ ROCKSDB_NAMESPACE::Env::Priority::LOW);
+
+ // Choose a location for the test database if none given with --db=<path>
+ if (FLAGS_db.empty()) {
+ std::string default_db_path;
+ FLAGS_env->GetTestDirectory(&default_db_path);
+ default_db_path += "/dbbench";
+ FLAGS_db = default_db_path;
+ }
+
+ if (FLAGS_backup_dir.empty()) {
+ FLAGS_backup_dir = FLAGS_db + "/backup";
+ }
+
+ if (FLAGS_restore_dir.empty()) {
+ FLAGS_restore_dir = FLAGS_db + "/restore";
+ }
+
+ if (FLAGS_stats_interval_seconds > 0) {
+ // When both are set then FLAGS_stats_interval determines the frequency
+ // at which the timer is checked for FLAGS_stats_interval_seconds
+ FLAGS_stats_interval = 1000;
+ }
+
+ if (FLAGS_seek_missing_prefix && FLAGS_prefix_size <= 8) {
+ fprintf(stderr, "prefix_size > 8 required by --seek_missing_prefix\n");
+ exit(1);
+ }
+
+ ROCKSDB_NAMESPACE::Benchmark benchmark;
+ benchmark.Run();
+
+#ifndef ROCKSDB_LITE
+ if (FLAGS_print_malloc_stats) {
+ std::string stats_string;
+ ROCKSDB_NAMESPACE::DumpMallocStats(&stats_string);
+ fprintf(stdout, "Malloc stats:\n%s\n", stats_string.c_str());
+ }
+#endif // ROCKSDB_LITE
+
+ return 0;
+}
+} // namespace ROCKSDB_NAMESPACE
+#endif
diff --git a/src/rocksdb/tools/db_bench_tool_test.cc b/src/rocksdb/tools/db_bench_tool_test.cc
new file mode 100644
index 000000000..a406ff66c
--- /dev/null
+++ b/src/rocksdb/tools/db_bench_tool_test.cc
@@ -0,0 +1,334 @@
+// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+// This source code is licensed under both the GPLv2 (found in the
+// COPYING file in the root directory) and Apache 2.0 License
+// (found in the LICENSE.Apache file in the root directory).
+//
+// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file. See the AUTHORS file for names of contributors.
+
+#include "rocksdb/db_bench_tool.h"
+
+#include "db/db_impl/db_impl.h"
+#include "options/options_parser.h"
+#include "rocksdb/utilities/options_util.h"
+#include "test_util/testharness.h"
+#include "test_util/testutil.h"
+#include "util/random.h"
+
+#ifdef GFLAGS
+#include "util/gflags_compat.h"
+
+namespace ROCKSDB_NAMESPACE {
+namespace {
+static const int kMaxArgCount = 100;
+static const size_t kArgBufferSize = 100000;
+} // namespace
+
+class DBBenchTest : public testing::Test {
+ public:
+ DBBenchTest() : rnd_(0xFB) {
+ test_path_ = test::PerThreadDBPath("db_bench_test");
+ Env::Default()->CreateDir(test_path_);
+ db_path_ = test_path_ + "/db";
+ wal_path_ = test_path_ + "/wal";
+ }
+
+ ~DBBenchTest() {
+ // DestroyDB(db_path_, Options());
+ }
+
+ void ResetArgs() {
+ argc_ = 0;
+ cursor_ = 0;
+ memset(arg_buffer_, 0, kArgBufferSize);
+ }
+
+ void AppendArgs(const std::vector<std::string>& args) {
+ for (const auto& arg : args) {
+ ASSERT_LE(cursor_ + arg.size() + 1, kArgBufferSize);
+ ASSERT_LE(argc_ + 1, kMaxArgCount);
+ snprintf(arg_buffer_ + cursor_, arg.size() + 1, "%s", arg.c_str());
+
+ argv_[argc_++] = arg_buffer_ + cursor_;
+ cursor_ += arg.size() + 1;
+ }
+ }
+
+ // Gets the default options for this test/db_bench.
+ // Note that db_bench may change some of the default option values and that
+ // the database might as well. The options changed by db_bench are
+ // specified here; the ones by the DB are set via SanitizeOptions
+ Options GetDefaultOptions(CompactionStyle style = kCompactionStyleLevel,
+ int levels = 7) const {
+ Options opt;
+
+ opt.create_if_missing = true;
+ opt.max_open_files = 256;
+ opt.max_background_compactions = 10;
+ opt.dump_malloc_stats = true; // db_bench uses a different default
+ opt.compaction_style = style;
+ opt.num_levels = levels;
+ opt.compression = kNoCompression;
+ opt.arena_block_size = 8388608;
+
+ return SanitizeOptions(db_path_, opt);
+ }
+
+ void RunDbBench(const std::string& options_file_name) {
+ AppendArgs({"./db_bench", "--benchmarks=fillseq", "--use_existing_db=0",
+ "--num=1000", "--compression_type=none",
+ std::string(std::string("--db=") + db_path_).c_str(),
+ std::string(std::string("--wal_dir=") + wal_path_).c_str(),
+ std::string(std::string("--options_file=") + options_file_name)
+ .c_str()});
+ ASSERT_EQ(0, db_bench_tool(argc(), argv()));
+ }
+
+ void VerifyOptions(const Options& opt) {
+ DBOptions loaded_db_opts;
+ std::vector<ColumnFamilyDescriptor> cf_descs;
+ ASSERT_OK(LoadLatestOptions(db_path_, Env::Default(), &loaded_db_opts,
+ &cf_descs));
+
+ ConfigOptions exact;
+ exact.input_strings_escaped = false;
+ exact.sanity_level = ConfigOptions::kSanityLevelExactMatch;
+ ASSERT_OK(RocksDBOptionsParser::VerifyDBOptions(exact, DBOptions(opt),
+ loaded_db_opts));
+ ASSERT_OK(RocksDBOptionsParser::VerifyCFOptions(
+ exact, ColumnFamilyOptions(opt), cf_descs[0].options));
+
+ // check with the default rocksdb options and expect failure
+ ASSERT_NOK(RocksDBOptionsParser::VerifyDBOptions(exact, DBOptions(),
+ loaded_db_opts));
+ ASSERT_NOK(RocksDBOptionsParser::VerifyCFOptions(
+ exact, ColumnFamilyOptions(), cf_descs[0].options));
+ }
+
+ char** argv() { return argv_; }
+
+ int argc() { return argc_; }
+
+ std::string db_path_;
+ std::string test_path_;
+ std::string wal_path_;
+
+ char arg_buffer_[kArgBufferSize];
+ char* argv_[kMaxArgCount];
+ int argc_ = 0;
+ int cursor_ = 0;
+ Random rnd_;
+};
+
+namespace {} // namespace
+
+TEST_F(DBBenchTest, OptionsFile) {
+ const std::string kOptionsFileName = test_path_ + "/OPTIONS_test";
+ Options opt = GetDefaultOptions();
+ ASSERT_OK(PersistRocksDBOptions(DBOptions(opt), {"default"},
+ {ColumnFamilyOptions(opt)}, kOptionsFileName,
+ opt.env->GetFileSystem().get()));
+
+ // override the following options as db_bench will not take these
+ // options from the options file
+ opt.wal_dir = wal_path_;
+
+ RunDbBench(kOptionsFileName);
+ opt.delayed_write_rate = 16 * 1024 * 1024; // Set by SanitizeOptions
+
+ VerifyOptions(opt);
+}
+
+TEST_F(DBBenchTest, OptionsFileUniversal) {
+ const std::string kOptionsFileName = test_path_ + "/OPTIONS_test";
+
+ Options opt = GetDefaultOptions(kCompactionStyleUniversal, 1);
+
+ ASSERT_OK(PersistRocksDBOptions(DBOptions(opt), {"default"},
+ {ColumnFamilyOptions(opt)}, kOptionsFileName,
+ opt.env->GetFileSystem().get()));
+
+ // override the following options as db_bench will not take these
+ // options from the options file
+ opt.wal_dir = wal_path_;
+ RunDbBench(kOptionsFileName);
+
+ VerifyOptions(opt);
+}
+
+TEST_F(DBBenchTest, OptionsFileMultiLevelUniversal) {
+ const std::string kOptionsFileName = test_path_ + "/OPTIONS_test";
+
+ Options opt = GetDefaultOptions(kCompactionStyleUniversal, 12);
+
+ ASSERT_OK(PersistRocksDBOptions(DBOptions(opt), {"default"},
+ {ColumnFamilyOptions(opt)}, kOptionsFileName,
+ opt.env->GetFileSystem().get()));
+
+ // override the following options as db_bench will not take these
+ // options from the options file
+ opt.wal_dir = wal_path_;
+
+ RunDbBench(kOptionsFileName);
+ VerifyOptions(opt);
+}
+
+const std::string options_file_content = R"OPTIONS_FILE(
+[Version]
+ rocksdb_version=4.3.1
+ options_file_version=1.1
+
+[DBOptions]
+ wal_bytes_per_sync=1048576
+ delete_obsolete_files_period_micros=0
+ WAL_ttl_seconds=0
+ WAL_size_limit_MB=0
+ db_write_buffer_size=0
+ max_subcompactions=1
+ table_cache_numshardbits=4
+ max_open_files=-1
+ max_file_opening_threads=10
+ max_background_compactions=5
+ use_fsync=false
+ use_adaptive_mutex=false
+ max_total_wal_size=18446744073709551615
+ compaction_readahead_size=0
+ keep_log_file_num=10
+ skip_stats_update_on_db_open=false
+ max_manifest_file_size=18446744073709551615
+ db_log_dir=
+ writable_file_max_buffer_size=1048576
+ paranoid_checks=true
+ is_fd_close_on_exec=true
+ bytes_per_sync=1048576
+ enable_thread_tracking=true
+ recycle_log_file_num=0
+ create_missing_column_families=false
+ log_file_time_to_roll=0
+ max_background_flushes=1
+ create_if_missing=true
+ error_if_exists=false
+ delayed_write_rate=1048576
+ manifest_preallocation_size=4194304
+ allow_mmap_reads=false
+ allow_mmap_writes=false
+ use_direct_reads=false
+ use_direct_io_for_flush_and_compaction=false
+ stats_dump_period_sec=600
+ allow_fallocate=true
+ max_log_file_size=83886080
+ random_access_max_buffer_size=1048576
+ advise_random_on_open=true
+ dump_malloc_stats=true
+
+[CFOptions "default"]
+ compaction_filter_factory=nullptr
+ table_factory=BlockBasedTable
+ prefix_extractor=nullptr
+ comparator=leveldb.BytewiseComparator
+ compression_per_level=
+ max_bytes_for_level_base=104857600
+ bloom_locality=0
+ target_file_size_base=10485760
+ memtable_huge_page_size=0
+ max_successive_merges=1000
+ max_sequential_skip_in_iterations=8
+ arena_block_size=52428800
+ target_file_size_multiplier=1
+ source_compaction_factor=1
+ min_write_buffer_number_to_merge=1
+ max_write_buffer_number=2
+ write_buffer_size=419430400
+ max_grandparent_overlap_factor=10
+ max_bytes_for_level_multiplier=10
+ memtable_factory=SkipListFactory
+ compression=kNoCompression
+ min_partial_merge_operands=2
+ level0_stop_writes_trigger=100
+ num_levels=1
+ level0_slowdown_writes_trigger=50
+ level0_file_num_compaction_trigger=10
+ expanded_compaction_factor=25
+ max_write_buffer_number_to_maintain=0
+ max_write_buffer_size_to_maintain=0
+ verify_checksums_in_compaction=true
+ merge_operator=nullptr
+ memtable_prefix_bloom_bits=0
+ memtable_whole_key_filtering=true
+ paranoid_file_checks=false
+ inplace_update_num_locks=10000
+ optimize_filters_for_hits=false
+ level_compaction_dynamic_level_bytes=false
+ inplace_update_support=false
+ compaction_style=kCompactionStyleUniversal
+ memtable_prefix_bloom_probes=6
+ filter_deletes=false
+ hard_pending_compaction_bytes_limit=0
+ disable_auto_compactions=false
+ compaction_measure_io_stats=false
+ enable_blob_files=true
+ min_blob_size=16
+ blob_file_size=10485760
+ blob_compression_type=kNoCompression
+ enable_blob_garbage_collection=true
+ blob_garbage_collection_age_cutoff=0.5
+ blob_garbage_collection_force_threshold=0.75
+ blob_compaction_readahead_size=262144
+ blob_file_starting_level=0
+ prepopulate_blob_cache=kDisable;
+
+[TableOptions/BlockBasedTable "default"]
+ format_version=0
+ skip_table_builder_flush=false
+ cache_index_and_filter_blocks=false
+ flush_block_policy_factory=FlushBlockBySizePolicyFactory
+ index_type=kBinarySearch
+ whole_key_filtering=true
+ checksum=kCRC32c
+ no_block_cache=false
+ block_size=32768
+ block_size_deviation=10
+ block_restart_interval=16
+ filter_policy=rocksdb.BuiltinBloomFilter
+)OPTIONS_FILE";
+
+TEST_F(DBBenchTest, OptionsFileFromFile) {
+ const std::string kOptionsFileName = test_path_ + "/OPTIONS_flash";
+ std::unique_ptr<WritableFile> writable;
+ ASSERT_OK(Env::Default()->NewWritableFile(kOptionsFileName, &writable,
+ EnvOptions()));
+ ASSERT_OK(writable->Append(options_file_content));
+ ASSERT_OK(writable->Close());
+
+ DBOptions db_opt;
+ std::vector<ColumnFamilyDescriptor> cf_descs;
+ ASSERT_OK(LoadOptionsFromFile(kOptionsFileName, Env::Default(), &db_opt,
+ &cf_descs));
+ Options opt(db_opt, cf_descs[0].options);
+ opt.create_if_missing = true;
+
+ // override the following options as db_bench will not take these
+ // options from the options file
+ opt.wal_dir = wal_path_;
+
+ RunDbBench(kOptionsFileName);
+
+ VerifyOptions(SanitizeOptions(db_path_, opt));
+}
+
+} // namespace ROCKSDB_NAMESPACE
+
+int main(int argc, char** argv) {
+ ROCKSDB_NAMESPACE::port::InstallStackTraceHandler();
+ ::testing::InitGoogleTest(&argc, argv);
+ GFLAGS_NAMESPACE::ParseCommandLineFlags(&argc, &argv, true);
+ return RUN_ALL_TESTS();
+}
+
+#else
+
+int main(int argc, char** argv) {
+ printf("Skip db_bench_tool_test as the required library GFLAG is missing.");
+}
+#endif // #ifdef GFLAGS
diff --git a/src/rocksdb/tools/db_crashtest.py b/src/rocksdb/tools/db_crashtest.py
new file mode 100644
index 000000000..7035908cb
--- /dev/null
+++ b/src/rocksdb/tools/db_crashtest.py
@@ -0,0 +1,1016 @@
+#!/usr/bin/env python3
+# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+import argparse
+
+import os
+import random
+import shutil
+import subprocess
+import sys
+import tempfile
+import time
+
+# params overwrite priority:
+# for default:
+# default_params < {blackbox,whitebox}_default_params < args
+# for simple:
+# default_params < {blackbox,whitebox}_default_params <
+# simple_default_params <
+# {blackbox,whitebox}_simple_default_params < args
+# for cf_consistency:
+# default_params < {blackbox,whitebox}_default_params <
+# cf_consistency_params < args
+# for txn:
+# default_params < {blackbox,whitebox}_default_params < txn_params < args
+# for ts:
+# default_params < {blackbox,whitebox}_default_params < ts_params < args
+# for multiops_txn:
+# default_params < {blackbox,whitebox}_default_params < multiops_txn_params < args
+
+
+default_params = {
+ "acquire_snapshot_one_in": 10000,
+ "backup_max_size": 100 * 1024 * 1024,
+ # Consider larger number when backups considered more stable
+ "backup_one_in": 100000,
+ "batch_protection_bytes_per_key": lambda: random.choice([0, 8]),
+ "memtable_protection_bytes_per_key": lambda: random.choice([0, 1, 2, 4, 8]),
+ "block_size": 16384,
+ "bloom_bits": lambda: random.choice(
+ [random.randint(0, 19), random.lognormvariate(2.3, 1.3)]
+ ),
+ "cache_index_and_filter_blocks": lambda: random.randint(0, 1),
+ "cache_size": 8388608,
+ "charge_compression_dictionary_building_buffer": lambda: random.choice([0, 1]),
+ "charge_filter_construction": lambda: random.choice([0, 1]),
+ "charge_table_reader": lambda: random.choice([0, 1]),
+ "charge_file_metadata": lambda: random.choice([0, 1]),
+ "checkpoint_one_in": 1000000,
+ "compression_type": lambda: random.choice(
+ ["none", "snappy", "zlib", "lz4", "lz4hc", "xpress", "zstd"]
+ ),
+ "bottommost_compression_type": lambda: "disable"
+ if random.randint(0, 1) == 0
+ else random.choice(["none", "snappy", "zlib", "lz4", "lz4hc", "xpress", "zstd"]),
+ "checksum_type": lambda: random.choice(
+ ["kCRC32c", "kxxHash", "kxxHash64", "kXXH3"]
+ ),
+ "compression_max_dict_bytes": lambda: 16384 * random.randint(0, 1),
+ "compression_zstd_max_train_bytes": lambda: 65536 * random.randint(0, 1),
+ # Disabled compression_parallel_threads as the feature is not stable
+ # lambda: random.choice([1] * 9 + [4])
+ "compression_parallel_threads": 1,
+ "compression_max_dict_buffer_bytes": lambda: (1 << random.randint(0, 40)) - 1,
+ "compression_use_zstd_dict_trainer": lambda: random.randint(0, 1),
+ "clear_column_family_one_in": 0,
+ "compact_files_one_in": 1000000,
+ "compact_range_one_in": 1000000,
+ "compaction_pri": random.randint(0, 4),
+ "data_block_index_type": lambda: random.choice([0, 1]),
+ "delpercent": 4,
+ "delrangepercent": 1,
+ "destroy_db_initially": 0,
+ "enable_pipelined_write": lambda: random.randint(0, 1),
+ "enable_compaction_filter": lambda: random.choice([0, 0, 0, 1]),
+ "expected_values_dir": lambda: setup_expected_values_dir(),
+ "fail_if_options_file_error": lambda: random.randint(0, 1),
+ "flush_one_in": 1000000,
+ "manual_wal_flush_one_in": lambda: random.choice([0, 0, 1000, 1000000]),
+ "file_checksum_impl": lambda: random.choice(["none", "crc32c", "xxh64", "big"]),
+ "get_live_files_one_in": 1000000,
+ # Note: the following two are intentionally disabled as the corresponding
+ # APIs are not guaranteed to succeed.
+ "get_sorted_wal_files_one_in": 0,
+ "get_current_wal_file_one_in": 0,
+ # Temporarily disable hash index
+ "index_type": lambda: random.choice([0, 0, 0, 2, 2, 3]),
+ "ingest_external_file_one_in": 1000000,
+ "iterpercent": 10,
+ "mark_for_compaction_one_file_in": lambda: 10 * random.randint(0, 1),
+ "max_background_compactions": 20,
+ "max_bytes_for_level_base": 10485760,
+ "max_key": 25000000,
+ "max_write_buffer_number": 3,
+ "mmap_read": lambda: random.randint(0, 1),
+ # Setting `nooverwritepercent > 0` is only possible because we do not vary
+ # the random seed, so the same keys are chosen by every run for disallowing
+ # overwrites.
+ "nooverwritepercent": 1,
+ "open_files": lambda: random.choice([-1, -1, 100, 500000]),
+ "optimize_filters_for_memory": lambda: random.randint(0, 1),
+ "partition_filters": lambda: random.randint(0, 1),
+ "partition_pinning": lambda: random.randint(0, 3),
+ "pause_background_one_in": 1000000,
+ "prefix_size": lambda: random.choice([-1, 1, 5, 7, 8]),
+ "prefixpercent": 5,
+ "progress_reports": 0,
+ "readpercent": 45,
+ "recycle_log_file_num": lambda: random.randint(0, 1),
+ "snapshot_hold_ops": 100000,
+ "sst_file_manager_bytes_per_sec": lambda: random.choice([0, 104857600]),
+ "sst_file_manager_bytes_per_truncate": lambda: random.choice([0, 1048576]),
+ "long_running_snapshots": lambda: random.randint(0, 1),
+ "subcompactions": lambda: random.randint(1, 4),
+ "target_file_size_base": 2097152,
+ "target_file_size_multiplier": 2,
+ "test_batches_snapshots": random.randint(0, 1),
+ "top_level_index_pinning": lambda: random.randint(0, 3),
+ "unpartitioned_pinning": lambda: random.randint(0, 3),
+ "use_direct_reads": lambda: random.randint(0, 1),
+ "use_direct_io_for_flush_and_compaction": lambda: random.randint(0, 1),
+ "mock_direct_io": False,
+ "cache_type": lambda: random.choice(["lru_cache", "hyper_clock_cache"]),
+ "use_full_merge_v1": lambda: random.randint(0, 1),
+ "use_merge": lambda: random.randint(0, 1),
+ # use_put_entity_one_in has to be the same across invocations for verification to work, hence no lambda
+ "use_put_entity_one_in": random.choice([0] * 7 + [1, 5, 10]),
+ # 999 -> use Bloom API
+ "ribbon_starting_level": lambda: random.choice([random.randint(-1, 10), 999]),
+ "value_size_mult": 32,
+ "verify_checksum": 1,
+ "write_buffer_size": 4 * 1024 * 1024,
+ "writepercent": 35,
+ "format_version": lambda: random.choice([2, 3, 4, 5, 5]),
+ "index_block_restart_interval": lambda: random.choice(range(1, 16)),
+ "use_multiget": lambda: random.randint(0, 1),
+ "periodic_compaction_seconds": lambda: random.choice([0, 0, 1, 2, 10, 100, 1000]),
+ # 0 = never (used by some), 10 = often (for threading bugs), 600 = default
+ "stats_dump_period_sec": lambda: random.choice([0, 10, 600]),
+ "compaction_ttl": lambda: random.choice([0, 0, 1, 2, 10, 100, 1000]),
+ # Test small max_manifest_file_size in a smaller chance, as most of the
+ # time we wnat manifest history to be preserved to help debug
+ "max_manifest_file_size": lambda: random.choice(
+ [t * 16384 if t < 3 else 1024 * 1024 * 1024 for t in range(1, 30)]
+ ),
+ # Sync mode might make test runs slower so running it in a smaller chance
+ "sync": lambda: random.choice([1 if t == 0 else 0 for t in range(0, 20)]),
+ "bytes_per_sync": lambda: random.choice([0, 262144]),
+ "wal_bytes_per_sync": lambda: random.choice([0, 524288]),
+ # Disable compaction_readahead_size because the test is not passing.
+ # "compaction_readahead_size" : lambda : random.choice(
+ # [0, 0, 1024 * 1024]),
+ "db_write_buffer_size": lambda: random.choice(
+ [0, 0, 0, 1024 * 1024, 8 * 1024 * 1024, 128 * 1024 * 1024]
+ ),
+ "avoid_unnecessary_blocking_io": random.randint(0, 1),
+ "write_dbid_to_manifest": random.randint(0, 1),
+ "avoid_flush_during_recovery": lambda: random.choice(
+ [1 if t == 0 else 0 for t in range(0, 8)]
+ ),
+ "max_write_batch_group_size_bytes": lambda: random.choice(
+ [16, 64, 1024 * 1024, 16 * 1024 * 1024]
+ ),
+ "level_compaction_dynamic_level_bytes": True,
+ "verify_checksum_one_in": 1000000,
+ "verify_db_one_in": 100000,
+ "continuous_verification_interval": 0,
+ "max_key_len": 3,
+ "key_len_percent_dist": "1,30,69",
+ "read_fault_one_in": lambda: random.choice([0, 32, 1000]),
+ "open_metadata_write_fault_one_in": lambda: random.choice([0, 0, 8]),
+ "open_write_fault_one_in": lambda: random.choice([0, 0, 16]),
+ "open_read_fault_one_in": lambda: random.choice([0, 0, 32]),
+ "sync_fault_injection": lambda: random.randint(0, 1),
+ "get_property_one_in": 1000000,
+ "paranoid_file_checks": lambda: random.choice([0, 1, 1, 1]),
+ "max_write_buffer_size_to_maintain": lambda: random.choice(
+ [0, 1024 * 1024, 2 * 1024 * 1024, 4 * 1024 * 1024, 8 * 1024 * 1024]
+ ),
+ "user_timestamp_size": 0,
+ "secondary_cache_fault_one_in": lambda: random.choice([0, 0, 32]),
+ "prepopulate_block_cache": lambda: random.choice([0, 1]),
+ "memtable_prefix_bloom_size_ratio": lambda: random.choice([0.001, 0.01, 0.1, 0.5]),
+ "memtable_whole_key_filtering": lambda: random.randint(0, 1),
+ "detect_filter_construct_corruption": lambda: random.choice([0, 1]),
+ "adaptive_readahead": lambda: random.choice([0, 1]),
+ "async_io": lambda: random.choice([0, 1]),
+ "wal_compression": lambda: random.choice(["none", "zstd"]),
+ "verify_sst_unique_id_in_manifest": 1, # always do unique_id verification
+ "secondary_cache_uri": lambda: random.choice(
+ [
+ "",
+ "compressed_secondary_cache://capacity=8388608",
+ "compressed_secondary_cache://capacity=8388608;enable_custom_split_merge=true",
+ ]
+ ),
+ "allow_data_in_errors": True,
+ "readahead_size": lambda: random.choice([0, 16384, 524288]),
+ "initial_auto_readahead_size": lambda: random.choice([0, 16384, 524288]),
+ "max_auto_readahead_size": lambda: random.choice([0, 16384, 524288]),
+ "num_file_reads_for_auto_readahead": lambda: random.choice([0, 1, 2]),
+ "min_write_buffer_number_to_merge": lambda: random.choice([1, 2]),
+ "preserve_internal_time_seconds": lambda: random.choice([0, 60, 3600, 36000]),
+}
+
+_TEST_DIR_ENV_VAR = "TEST_TMPDIR"
+_DEBUG_LEVEL_ENV_VAR = "DEBUG_LEVEL"
+
+stress_cmd = "./db_stress"
+cleanup_cmd = None
+
+
+def is_release_mode():
+ return os.environ.get(_DEBUG_LEVEL_ENV_VAR) == "0"
+
+
+def get_dbname(test_name):
+ test_dir_name = "rocksdb_crashtest_" + test_name
+ test_tmpdir = os.environ.get(_TEST_DIR_ENV_VAR)
+ if test_tmpdir is None or test_tmpdir == "":
+ dbname = tempfile.mkdtemp(prefix=test_dir_name)
+ else:
+ dbname = test_tmpdir + "/" + test_dir_name
+ shutil.rmtree(dbname, True)
+ if cleanup_cmd is not None:
+ print("Running DB cleanup command - %s\n" % cleanup_cmd)
+ # Ignore failure
+ os.system(cleanup_cmd)
+ os.mkdir(dbname)
+ return dbname
+
+
+expected_values_dir = None
+
+
+def setup_expected_values_dir():
+ global expected_values_dir
+ if expected_values_dir is not None:
+ return expected_values_dir
+ expected_dir_prefix = "rocksdb_crashtest_expected_"
+ test_tmpdir = os.environ.get(_TEST_DIR_ENV_VAR)
+ if test_tmpdir is None or test_tmpdir == "":
+ expected_values_dir = tempfile.mkdtemp(prefix=expected_dir_prefix)
+ else:
+ # if tmpdir is specified, store the expected_values_dir under that dir
+ expected_values_dir = test_tmpdir + "/rocksdb_crashtest_expected"
+ if os.path.exists(expected_values_dir):
+ shutil.rmtree(expected_values_dir)
+ os.mkdir(expected_values_dir)
+ return expected_values_dir
+
+
+multiops_txn_key_spaces_file = None
+
+
+def setup_multiops_txn_key_spaces_file():
+ global multiops_txn_key_spaces_file
+ if multiops_txn_key_spaces_file is not None:
+ return multiops_txn_key_spaces_file
+ key_spaces_file_prefix = "rocksdb_crashtest_multiops_txn_key_spaces"
+ test_tmpdir = os.environ.get(_TEST_DIR_ENV_VAR)
+ if test_tmpdir is None or test_tmpdir == "":
+ multiops_txn_key_spaces_file = tempfile.mkstemp(prefix=key_spaces_file_prefix)[
+ 1
+ ]
+ else:
+ if not os.path.exists(test_tmpdir):
+ os.mkdir(test_tmpdir)
+ multiops_txn_key_spaces_file = tempfile.mkstemp(
+ prefix=key_spaces_file_prefix, dir=test_tmpdir
+ )[1]
+ return multiops_txn_key_spaces_file
+
+
+def is_direct_io_supported(dbname):
+ with tempfile.NamedTemporaryFile(dir=dbname) as f:
+ try:
+ os.open(f.name, os.O_DIRECT)
+ except BaseException:
+ return False
+ return True
+
+
+blackbox_default_params = {
+ "disable_wal": lambda: random.choice([0, 0, 0, 1]),
+ # total time for this script to test db_stress
+ "duration": 6000,
+ # time for one db_stress instance to run
+ "interval": 120,
+ # since we will be killing anyway, use large value for ops_per_thread
+ "ops_per_thread": 100000000,
+ "reopen": 0,
+ "set_options_one_in": 10000,
+}
+
+whitebox_default_params = {
+ # TODO: enable this once we figure out how to adjust kill odds for WAL-
+ # disabled runs, and either (1) separate full `db_stress` runs out of
+ # whitebox crash or (2) support verification at end of `db_stress` runs
+ # that ran with WAL disabled.
+ "disable_wal": 0,
+ "duration": 10000,
+ "log2_keys_per_lock": 10,
+ "ops_per_thread": 200000,
+ "random_kill_odd": 888887,
+ "reopen": 20,
+}
+
+simple_default_params = {
+ "allow_concurrent_memtable_write": lambda: random.randint(0, 1),
+ "column_families": 1,
+ # TODO: re-enable once internal task T124324915 is fixed.
+ # "experimental_mempurge_threshold": lambda: 10.0*random.random(),
+ "max_background_compactions": 1,
+ "max_bytes_for_level_base": 67108864,
+ "memtablerep": "skip_list",
+ "target_file_size_base": 16777216,
+ "target_file_size_multiplier": 1,
+ "test_batches_snapshots": 0,
+ "write_buffer_size": 32 * 1024 * 1024,
+ "level_compaction_dynamic_level_bytes": False,
+ "paranoid_file_checks": lambda: random.choice([0, 1, 1, 1]),
+ "verify_iterator_with_expected_state_one_in": 5, # this locks a range of keys
+}
+
+blackbox_simple_default_params = {
+ "open_files": -1,
+ "set_options_one_in": 0,
+}
+
+whitebox_simple_default_params = {}
+
+cf_consistency_params = {
+ "disable_wal": lambda: random.randint(0, 1),
+ "reopen": 0,
+ "test_cf_consistency": 1,
+ # use small value for write_buffer_size so that RocksDB triggers flush
+ # more frequently
+ "write_buffer_size": 1024 * 1024,
+ "enable_pipelined_write": lambda: random.randint(0, 1),
+ # Snapshots are used heavily in this test mode, while they are incompatible
+ # with compaction filter.
+ "enable_compaction_filter": 0,
+ # `CfConsistencyStressTest::TestIngestExternalFile()` is not implemented.
+ "ingest_external_file_one_in": 0,
+}
+
+txn_params = {
+ "use_txn": 1,
+ # Avoid lambda to set it once for the entire test
+ "txn_write_policy": random.randint(0, 2),
+ "unordered_write": random.randint(0, 1),
+ # TODO: there is such a thing as transactions with WAL disabled. We should
+ # cover that case.
+ "disable_wal": 0,
+ # OpenReadOnly after checkpoint is not currnetly compatible with WritePrepared txns
+ "checkpoint_one_in": 0,
+ # pipeline write is not currnetly compatible with WritePrepared txns
+ "enable_pipelined_write": 0,
+ "create_timestamped_snapshot_one_in": random.choice([0, 20]),
+ # PutEntity in transactions is not yet implemented
+ "use_put_entity_one_in" : 0,
+}
+
+best_efforts_recovery_params = {
+ "best_efforts_recovery": 1,
+ "atomic_flush": 0,
+ "disable_wal": 1,
+ "column_families": 1,
+}
+
+blob_params = {
+ "allow_setting_blob_options_dynamically": 1,
+ # Enable blob files and GC with a 75% chance initially; note that they might still be
+ # enabled/disabled during the test via SetOptions
+ "enable_blob_files": lambda: random.choice([0] + [1] * 3),
+ "min_blob_size": lambda: random.choice([0, 8, 16]),
+ "blob_file_size": lambda: random.choice([1048576, 16777216, 268435456, 1073741824]),
+ "blob_compression_type": lambda: random.choice(["none", "snappy", "lz4", "zstd"]),
+ "enable_blob_garbage_collection": lambda: random.choice([0] + [1] * 3),
+ "blob_garbage_collection_age_cutoff": lambda: random.choice(
+ [0.0, 0.25, 0.5, 0.75, 1.0]
+ ),
+ "blob_garbage_collection_force_threshold": lambda: random.choice([0.5, 0.75, 1.0]),
+ "blob_compaction_readahead_size": lambda: random.choice([0, 1048576, 4194304]),
+ "blob_file_starting_level": lambda: random.choice(
+ [0] * 4 + [1] * 3 + [2] * 2 + [3]
+ ),
+ "use_blob_cache": lambda: random.randint(0, 1),
+ "use_shared_block_and_blob_cache": lambda: random.randint(0, 1),
+ "blob_cache_size": lambda: random.choice([1048576, 2097152, 4194304, 8388608]),
+ "prepopulate_blob_cache": lambda: random.randint(0, 1),
+}
+
+ts_params = {
+ "test_cf_consistency": 0,
+ "test_batches_snapshots": 0,
+ "user_timestamp_size": 8,
+ "use_merge": 0,
+ "use_full_merge_v1": 0,
+ "use_txn": 0,
+ "enable_blob_files": 0,
+ "use_blob_db": 0,
+ "ingest_external_file_one_in": 0,
+ # PutEntity with timestamps is not yet implemented
+ "use_put_entity_one_in" : 0,
+}
+
+tiered_params = {
+ "enable_tiered_storage": 1,
+ # Set tiered compaction hot data time as: 1 minute, 1 hour, 10 hour
+ "preclude_last_level_data_seconds": lambda: random.choice([60, 3600, 36000]),
+ # only test universal compaction for now, level has known issue of
+ # endless compaction
+ "compaction_style": 1,
+ # tiered storage doesn't support blob db yet
+ "enable_blob_files": 0,
+ "use_blob_db": 0,
+}
+
+multiops_txn_default_params = {
+ "test_cf_consistency": 0,
+ "test_batches_snapshots": 0,
+ "test_multi_ops_txns": 1,
+ "use_txn": 1,
+ "two_write_queues": lambda: random.choice([0, 1]),
+ # TODO: enable write-prepared
+ "disable_wal": 0,
+ "use_only_the_last_commit_time_batch_for_recovery": lambda: random.choice([0, 1]),
+ "clear_column_family_one_in": 0,
+ "column_families": 1,
+ "enable_pipelined_write": lambda: random.choice([0, 1]),
+ # This test already acquires snapshots in reads
+ "acquire_snapshot_one_in": 0,
+ "backup_one_in": 0,
+ "writepercent": 0,
+ "delpercent": 0,
+ "delrangepercent": 0,
+ "customopspercent": 80,
+ "readpercent": 5,
+ "iterpercent": 15,
+ "prefixpercent": 0,
+ "verify_db_one_in": 1000,
+ "continuous_verification_interval": 1000,
+ "delay_snapshot_read_one_in": 3,
+ # 65536 is the smallest possible value for write_buffer_size. Smaller
+ # values will be sanitized to 65536 during db open. SetOptions currently
+ # does not sanitize options, but very small write_buffer_size may cause
+ # assertion failure in
+ # https://github.com/facebook/rocksdb/blob/7.0.fb/db/memtable.cc#L117.
+ "write_buffer_size": 65536,
+ # flush more frequently to generate more files, thus trigger more
+ # compactions.
+ "flush_one_in": 1000,
+ "key_spaces_path": setup_multiops_txn_key_spaces_file(),
+ "rollback_one_in": 4,
+ # Re-enable once we have a compaction for MultiOpsTxnStressTest
+ "enable_compaction_filter": 0,
+ "create_timestamped_snapshot_one_in": 50,
+ "sync_fault_injection": 0,
+ # PutEntity in transactions is not yet implemented
+ "use_put_entity_one_in" : 0,
+}
+
+multiops_wc_txn_params = {
+ "txn_write_policy": 0,
+ # TODO re-enable pipelined write. Not well tested atm
+ "enable_pipelined_write": 0,
+}
+
+multiops_wp_txn_params = {
+ "txn_write_policy": 1,
+ "wp_snapshot_cache_bits": 1,
+ # try small wp_commit_cache_bits, e.g. 0 once we explore storing full
+ # commit sequence numbers in commit cache
+ "wp_commit_cache_bits": 10,
+ # pipeline write is not currnetly compatible with WritePrepared txns
+ "enable_pipelined_write": 0,
+ # OpenReadOnly after checkpoint is not currnetly compatible with WritePrepared txns
+ "checkpoint_one_in": 0,
+ # Required to be 1 in order to use commit-time-batch
+ "use_only_the_last_commit_time_batch_for_recovery": 1,
+ "clear_wp_commit_cache_one_in": 10,
+ "create_timestamped_snapshot_one_in": 0,
+}
+
+
+def finalize_and_sanitize(src_params):
+ dest_params = {k: v() if callable(v) else v for (k, v) in src_params.items()}
+ if is_release_mode():
+ dest_params["read_fault_one_in"] = 0
+ if dest_params.get("compression_max_dict_bytes") == 0:
+ dest_params["compression_zstd_max_train_bytes"] = 0
+ dest_params["compression_max_dict_buffer_bytes"] = 0
+ if dest_params.get("compression_type") != "zstd":
+ dest_params["compression_zstd_max_train_bytes"] = 0
+ if dest_params.get("allow_concurrent_memtable_write", 1) == 1:
+ dest_params["memtablerep"] = "skip_list"
+ if dest_params["mmap_read"] == 1:
+ dest_params["use_direct_io_for_flush_and_compaction"] = 0
+ dest_params["use_direct_reads"] = 0
+ if dest_params["file_checksum_impl"] != "none":
+ # TODO(T109283569): there is a bug in `GenerateOneFileChecksum()`,
+ # used by `IngestExternalFile()`, causing it to fail with mmap
+ # reads. Remove this once it is fixed.
+ dest_params["ingest_external_file_one_in"] = 0
+ if (
+ dest_params["use_direct_io_for_flush_and_compaction"] == 1
+ or dest_params["use_direct_reads"] == 1
+ ) and not is_direct_io_supported(dest_params["db"]):
+ if is_release_mode():
+ print(
+ "{} does not support direct IO. Disabling use_direct_reads and "
+ "use_direct_io_for_flush_and_compaction.\n".format(dest_params["db"])
+ )
+ dest_params["use_direct_reads"] = 0
+ dest_params["use_direct_io_for_flush_and_compaction"] = 0
+ else:
+ dest_params["mock_direct_io"] = True
+
+ if dest_params["test_batches_snapshots"] == 1:
+ dest_params["enable_compaction_filter"] = 0
+ if dest_params["prefix_size"] < 0:
+ dest_params["prefix_size"] = 1
+
+ # Multi-key operations are not currently compatible with transactions or
+ # timestamp.
+ if (dest_params.get("test_batches_snapshots") == 1 or
+ dest_params.get("use_txn") == 1 or
+ dest_params.get("user_timestamp_size") > 0):
+ dest_params["ingest_external_file_one_in"] = 0
+ if (dest_params.get("test_batches_snapshots") == 1 or
+ dest_params.get("use_txn") == 1):
+ dest_params["delpercent"] += dest_params["delrangepercent"]
+ dest_params["delrangepercent"] = 0
+ if (
+ dest_params.get("disable_wal") == 1
+ or dest_params.get("sync_fault_injection") == 1
+ or dest_params.get("manual_wal_flush_one_in") > 0
+ ):
+ # File ingestion does not guarantee prefix-recoverability when unsynced
+ # data can be lost. Ingesting a file syncs data immediately that is
+ # newer than unsynced memtable data that can be lost on restart.
+ #
+ # Even if the above issue is fixed or worked around, our
+ # trace-and-replay does not trace file ingestion, so in its current form
+ # it would not recover the expected state to the correct point in time.
+ dest_params["ingest_external_file_one_in"] = 0
+ # The `DbStressCompactionFilter` can apply memtable updates to SST
+ # files, which would be problematic when unsynced data can be lost in
+ # crash recoveries.
+ dest_params["enable_compaction_filter"] = 0
+ # Only under WritePrepared txns, unordered_write would provide the same guarnatees as vanilla rocksdb
+ if dest_params.get("unordered_write", 0) == 1:
+ dest_params["txn_write_policy"] = 1
+ dest_params["allow_concurrent_memtable_write"] = 1
+ if dest_params.get("disable_wal", 0) == 1:
+ dest_params["atomic_flush"] = 1
+ dest_params["sync"] = 0
+ dest_params["write_fault_one_in"] = 0
+ if dest_params.get("open_files", 1) != -1:
+ # Compaction TTL and periodic compactions are only compatible
+ # with open_files = -1
+ dest_params["compaction_ttl"] = 0
+ dest_params["periodic_compaction_seconds"] = 0
+ if dest_params.get("compaction_style", 0) == 2:
+ # Disable compaction TTL in FIFO compaction, because right
+ # now assertion failures are triggered.
+ dest_params["compaction_ttl"] = 0
+ dest_params["periodic_compaction_seconds"] = 0
+ if dest_params["partition_filters"] == 1:
+ if dest_params["index_type"] != 2:
+ dest_params["partition_filters"] = 0
+ if dest_params.get("atomic_flush", 0) == 1:
+ # disable pipelined write when atomic flush is used.
+ dest_params["enable_pipelined_write"] = 0
+ if dest_params.get("sst_file_manager_bytes_per_sec", 0) == 0:
+ dest_params["sst_file_manager_bytes_per_truncate"] = 0
+ if dest_params.get("enable_compaction_filter", 0) == 1:
+ # Compaction filter is incompatible with snapshots. Need to avoid taking
+ # snapshots, as well as avoid operations that use snapshots for
+ # verification.
+ dest_params["acquire_snapshot_one_in"] = 0
+ dest_params["compact_range_one_in"] = 0
+ # Give the iterator ops away to reads.
+ dest_params["readpercent"] += dest_params.get("iterpercent", 10)
+ dest_params["iterpercent"] = 0
+ if dest_params.get("prefix_size") == -1:
+ dest_params["readpercent"] += dest_params.get("prefixpercent", 20)
+ dest_params["prefixpercent"] = 0
+ if (
+ dest_params.get("prefix_size") == -1
+ and dest_params.get("memtable_whole_key_filtering") == 0
+ ):
+ dest_params["memtable_prefix_bloom_size_ratio"] = 0
+ if dest_params.get("two_write_queues") == 1:
+ dest_params["enable_pipelined_write"] = 0
+ if dest_params.get("best_efforts_recovery") == 1:
+ dest_params["disable_wal"] = 1
+ dest_params["atomic_flush"] = 0
+ dest_params["enable_compaction_filter"] = 0
+ dest_params["sync"] = 0
+ dest_params["write_fault_one_in"] = 0
+ if dest_params["secondary_cache_uri"] != "":
+ # Currently the only cache type compatible with a secondary cache is LRUCache
+ dest_params["cache_type"] = "lru_cache"
+ # Remove the following once write-prepared/write-unprepared with/without
+ # unordered write supports timestamped snapshots
+ if dest_params.get("create_timestamped_snapshot_one_in", 0) > 0:
+ dest_params["txn_write_policy"] = 0
+ dest_params["unordered_write"] = 0
+ # For TransactionDB, correctness testing with unsync data loss is currently
+ # compatible with only write committed policy
+ if (dest_params.get("use_txn") == 1 and dest_params.get("txn_write_policy") != 0):
+ dest_params["sync_fault_injection"] = 0
+ dest_params["manual_wal_flush_one_in"] = 0
+ # PutEntity is currently not supported by SstFileWriter or in conjunction with Merge
+ if dest_params["use_put_entity_one_in"] != 0:
+ dest_params["ingest_external_file_one_in"] = 0
+ dest_params["use_merge"] = 0
+ dest_params["use_full_merge_v1"] = 0
+
+ return dest_params
+
+
+def gen_cmd_params(args):
+ params = {}
+
+ params.update(default_params)
+ if args.test_type == "blackbox":
+ params.update(blackbox_default_params)
+ if args.test_type == "whitebox":
+ params.update(whitebox_default_params)
+ if args.simple:
+ params.update(simple_default_params)
+ if args.test_type == "blackbox":
+ params.update(blackbox_simple_default_params)
+ if args.test_type == "whitebox":
+ params.update(whitebox_simple_default_params)
+ if args.cf_consistency:
+ params.update(cf_consistency_params)
+ if args.txn:
+ params.update(txn_params)
+ if args.test_best_efforts_recovery:
+ params.update(best_efforts_recovery_params)
+ if args.enable_ts:
+ params.update(ts_params)
+ if args.test_multiops_txn:
+ params.update(multiops_txn_default_params)
+ if args.write_policy == "write_committed":
+ params.update(multiops_wc_txn_params)
+ elif args.write_policy == "write_prepared":
+ params.update(multiops_wp_txn_params)
+ if args.test_tiered_storage:
+ params.update(tiered_params)
+
+ # Best-effort recovery, user defined timestamp, tiered storage are currently
+ # incompatible with BlobDB. Test BE recovery if specified on the command
+ # line; otherwise, apply BlobDB related overrides with a 10% chance.
+ if (
+ not args.test_best_efforts_recovery
+ and not args.enable_ts
+ and not args.test_tiered_storage
+ and random.choice([0] * 9 + [1]) == 1
+ ):
+ params.update(blob_params)
+
+ for k, v in vars(args).items():
+ if v is not None:
+ params[k] = v
+ return params
+
+
+def gen_cmd(params, unknown_params):
+ finalzied_params = finalize_and_sanitize(params)
+ cmd = (
+ [stress_cmd]
+ + [
+ "--{0}={1}".format(k, v)
+ for k, v in [(k, finalzied_params[k]) for k in sorted(finalzied_params)]
+ if k
+ not in {
+ "test_type",
+ "simple",
+ "duration",
+ "interval",
+ "random_kill_odd",
+ "cf_consistency",
+ "txn",
+ "test_best_efforts_recovery",
+ "enable_ts",
+ "test_multiops_txn",
+ "write_policy",
+ "stress_cmd",
+ "test_tiered_storage",
+ "cleanup_cmd",
+ }
+ and v is not None
+ ]
+ + unknown_params
+ )
+ return cmd
+
+
+def execute_cmd(cmd, timeout):
+ child = subprocess.Popen(cmd, stderr=subprocess.PIPE, stdout=subprocess.PIPE)
+ print("Running db_stress with pid=%d: %s\n\n" % (child.pid, " ".join(cmd)))
+
+ try:
+ outs, errs = child.communicate(timeout=timeout)
+ hit_timeout = False
+ print("WARNING: db_stress ended before kill: exitcode=%d\n" % child.returncode)
+ except subprocess.TimeoutExpired:
+ hit_timeout = True
+ child.kill()
+ print("KILLED %d\n" % child.pid)
+ outs, errs = child.communicate()
+
+ return hit_timeout, child.returncode, outs.decode("utf-8"), errs.decode("utf-8")
+
+
+# This script runs and kills db_stress multiple times. It checks consistency
+# in case of unsafe crashes in RocksDB.
+def blackbox_crash_main(args, unknown_args):
+ cmd_params = gen_cmd_params(args)
+ dbname = get_dbname("blackbox")
+ exit_time = time.time() + cmd_params["duration"]
+
+ print(
+ "Running blackbox-crash-test with \n"
+ + "interval_between_crash="
+ + str(cmd_params["interval"])
+ + "\n"
+ + "total-duration="
+ + str(cmd_params["duration"])
+ + "\n"
+ )
+
+ while time.time() < exit_time:
+ cmd = gen_cmd(
+ dict(list(cmd_params.items()) + list({"db": dbname}.items())), unknown_args
+ )
+
+ hit_timeout, retcode, outs, errs = execute_cmd(cmd, cmd_params["interval"])
+
+ if not hit_timeout:
+ print("Exit Before Killing")
+ print("stdout:")
+ print(outs)
+ print("stderr:")
+ print(errs)
+ sys.exit(2)
+
+ for line in errs.split("\n"):
+ if line != "" and not line.startswith("WARNING"):
+ print("stderr has error message:")
+ print("***" + line + "***")
+
+ time.sleep(1) # time to stabilize before the next run
+
+ time.sleep(1) # time to stabilize before the next run
+
+ # we need to clean up after ourselves -- only do this on test success
+ shutil.rmtree(dbname, True)
+
+
+# This python script runs db_stress multiple times. Some runs with
+# kill_random_test that causes rocksdb to crash at various points in code.
+def whitebox_crash_main(args, unknown_args):
+ cmd_params = gen_cmd_params(args)
+ dbname = get_dbname("whitebox")
+
+ cur_time = time.time()
+ exit_time = cur_time + cmd_params["duration"]
+ half_time = cur_time + cmd_params["duration"] // 2
+
+ print(
+ "Running whitebox-crash-test with \n"
+ + "total-duration="
+ + str(cmd_params["duration"])
+ + "\n"
+ )
+
+ total_check_mode = 4
+ check_mode = 0
+ kill_random_test = cmd_params["random_kill_odd"]
+ kill_mode = 0
+ prev_compaction_style = -1
+ while time.time() < exit_time:
+ if check_mode == 0:
+ additional_opts = {
+ # use large ops per thread since we will kill it anyway
+ "ops_per_thread": 100
+ * cmd_params["ops_per_thread"],
+ }
+ # run with kill_random_test, with three modes.
+ # Mode 0 covers all kill points. Mode 1 covers less kill points but
+ # increases change of triggering them. Mode 2 covers even less
+ # frequent kill points and further increases triggering change.
+ if kill_mode == 0:
+ additional_opts.update(
+ {
+ "kill_random_test": kill_random_test,
+ }
+ )
+ elif kill_mode == 1:
+ if cmd_params.get("disable_wal", 0) == 1:
+ my_kill_odd = kill_random_test // 50 + 1
+ else:
+ my_kill_odd = kill_random_test // 10 + 1
+ additional_opts.update(
+ {
+ "kill_random_test": my_kill_odd,
+ "kill_exclude_prefixes": "WritableFileWriter::Append,"
+ + "WritableFileWriter::WriteBuffered",
+ }
+ )
+ elif kill_mode == 2:
+ # TODO: May need to adjust random odds if kill_random_test
+ # is too small.
+ additional_opts.update(
+ {
+ "kill_random_test": (kill_random_test // 5000 + 1),
+ "kill_exclude_prefixes": "WritableFileWriter::Append,"
+ "WritableFileWriter::WriteBuffered,"
+ "PosixMmapFile::Allocate,WritableFileWriter::Flush",
+ }
+ )
+ # Run kill mode 0, 1 and 2 by turn.
+ kill_mode = (kill_mode + 1) % 3
+ elif check_mode == 1:
+ # normal run with universal compaction mode
+ additional_opts = {
+ "kill_random_test": None,
+ "ops_per_thread": cmd_params["ops_per_thread"],
+ "compaction_style": 1,
+ }
+ # Single level universal has a lot of special logic. Ensure we cover
+ # it sometimes.
+ if random.randint(0, 1) == 1:
+ additional_opts.update(
+ {
+ "num_levels": 1,
+ }
+ )
+ elif check_mode == 2:
+ # normal run with FIFO compaction mode
+ # ops_per_thread is divided by 5 because FIFO compaction
+ # style is quite a bit slower on reads with lot of files
+ additional_opts = {
+ "kill_random_test": None,
+ "ops_per_thread": cmd_params["ops_per_thread"] // 5,
+ "compaction_style": 2,
+ }
+ else:
+ # normal run
+ additional_opts = {
+ "kill_random_test": None,
+ "ops_per_thread": cmd_params["ops_per_thread"],
+ }
+
+ cur_compaction_style = additional_opts.get("compaction_style", cmd_params.get("compaction_style", 0))
+ if prev_compaction_style != -1 and prev_compaction_style != cur_compaction_style:
+ print("`compaction_style` is changed in current run so `destroy_db_initially` is set to 1 as a short-term solution to avoid cycling through previous db of different compaction style." + "\n")
+ additional_opts["destroy_db_initially"] = 1
+ prev_compaction_style = cur_compaction_style
+
+ cmd = gen_cmd(
+ dict(
+ list(cmd_params.items())
+ + list(additional_opts.items())
+ + list({"db": dbname}.items())
+ ),
+ unknown_args,
+ )
+
+ print(
+ "Running:" + " ".join(cmd) + "\n"
+ ) # noqa: E999 T25377293 Grandfathered in
+
+ # If the running time is 15 minutes over the run time, explicit kill and
+ # exit even if white box kill didn't hit. This is to guarantee run time
+ # limit, as if it runs as a job, running too long will create problems
+ # for job scheduling or execution.
+ # TODO detect a hanging condition. The job might run too long as RocksDB
+ # hits a hanging bug.
+ hit_timeout, retncode, stdoutdata, stderrdata = execute_cmd(
+ cmd, exit_time - time.time() + 900
+ )
+ msg = "check_mode={0}, kill option={1}, exitcode={2}\n".format(
+ check_mode, additional_opts["kill_random_test"], retncode
+ )
+
+ print(msg)
+ print(stdoutdata)
+ print(stderrdata)
+
+ if hit_timeout:
+ print("Killing the run for running too long")
+ break
+
+ expected = False
+ if additional_opts["kill_random_test"] is None and (retncode == 0):
+ # we expect zero retncode if no kill option
+ expected = True
+ elif additional_opts["kill_random_test"] is not None and retncode <= 0:
+ # When kill option is given, the test MIGHT kill itself.
+ # If it does, negative retncode is expected. Otherwise 0.
+ expected = True
+
+ if not expected:
+ print("TEST FAILED. See kill option and exit code above!!!\n")
+ sys.exit(1)
+
+ stderrdata = stderrdata.lower()
+ errorcount = stderrdata.count("error") - stderrdata.count("got errors 0 times")
+ print("#times error occurred in output is " + str(errorcount) + "\n")
+
+ if errorcount > 0:
+ print("TEST FAILED. Output has 'error'!!!\n")
+ sys.exit(2)
+ if stderrdata.find("fail") >= 0:
+ print("TEST FAILED. Output has 'fail'!!!\n")
+ sys.exit(2)
+
+ # First half of the duration, keep doing kill test. For the next half,
+ # try different modes.
+ if time.time() > half_time:
+ # we need to clean up after ourselves -- only do this on test
+ # success
+ shutil.rmtree(dbname, True)
+ if cleanup_cmd is not None:
+ print("Running DB cleanup command - %s\n" % cleanup_cmd)
+ ret = os.system(cleanup_cmd)
+ if ret != 0:
+ print("TEST FAILED. DB cleanup returned error %d\n" % ret)
+ sys.exit(1)
+ os.mkdir(dbname)
+ if (expected_values_dir is not None):
+ shutil.rmtree(expected_values_dir, True)
+ os.mkdir(expected_values_dir)
+
+ check_mode = (check_mode + 1) % total_check_mode
+
+ time.sleep(1) # time to stabilize after a kill
+
+
+def main():
+ global stress_cmd
+ global cleanup_cmd
+
+ parser = argparse.ArgumentParser(
+ description="This script runs and kills \
+ db_stress multiple times"
+ )
+ parser.add_argument("test_type", choices=["blackbox", "whitebox"])
+ parser.add_argument("--simple", action="store_true")
+ parser.add_argument("--cf_consistency", action="store_true")
+ parser.add_argument("--txn", action="store_true")
+ parser.add_argument("--test_best_efforts_recovery", action="store_true")
+ parser.add_argument("--enable_ts", action="store_true")
+ parser.add_argument("--test_multiops_txn", action="store_true")
+ parser.add_argument("--write_policy", choices=["write_committed", "write_prepared"])
+ parser.add_argument("--stress_cmd")
+ parser.add_argument("--test_tiered_storage", action="store_true")
+ parser.add_argument("--cleanup_cmd")
+
+ all_params = dict(
+ list(default_params.items())
+ + list(blackbox_default_params.items())
+ + list(whitebox_default_params.items())
+ + list(simple_default_params.items())
+ + list(blackbox_simple_default_params.items())
+ + list(whitebox_simple_default_params.items())
+ + list(blob_params.items())
+ + list(ts_params.items())
+ + list(multiops_txn_default_params.items())
+ + list(multiops_wc_txn_params.items())
+ + list(multiops_wp_txn_params.items())
+ + list(best_efforts_recovery_params.items())
+ + list(cf_consistency_params.items())
+ + list(tiered_params.items())
+ + list(txn_params.items())
+ )
+
+ for k, v in all_params.items():
+ parser.add_argument("--" + k, type=type(v() if callable(v) else v))
+ # unknown_args are passed directly to db_stress
+ args, unknown_args = parser.parse_known_args()
+
+ test_tmpdir = os.environ.get(_TEST_DIR_ENV_VAR)
+ if test_tmpdir is not None and not os.path.isdir(test_tmpdir):
+ print(
+ "%s env var is set to a non-existent directory: %s"
+ % (_TEST_DIR_ENV_VAR, test_tmpdir)
+ )
+ sys.exit(1)
+
+ if args.stress_cmd:
+ stress_cmd = args.stress_cmd
+ if args.cleanup_cmd:
+ cleanup_cmd = args.cleanup_cmd
+ if args.test_type == "blackbox":
+ blackbox_crash_main(args, unknown_args)
+ if args.test_type == "whitebox":
+ whitebox_crash_main(args, unknown_args)
+ # Only delete the `expected_values_dir` if test passes
+ if expected_values_dir is not None:
+ shutil.rmtree(expected_values_dir)
+ if multiops_txn_key_spaces_file is not None:
+ os.remove(multiops_txn_key_spaces_file)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/rocksdb/tools/db_repl_stress.cc b/src/rocksdb/tools/db_repl_stress.cc
new file mode 100644
index 000000000..ba680f4f2
--- /dev/null
+++ b/src/rocksdb/tools/db_repl_stress.cc
@@ -0,0 +1,140 @@
+// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+// This source code is licensed under both the GPLv2 (found in the
+// COPYING file in the root directory) and Apache 2.0 License
+// (found in the LICENSE.Apache file in the root directory).
+
+#ifndef ROCKSDB_LITE
+#ifndef GFLAGS
+#include <cstdio>
+int main() {
+ fprintf(stderr, "Please install gflags to run rocksdb tools\n");
+ return 1;
+}
+#else
+
+#include <atomic>
+#include <cstdio>
+
+#include "db/write_batch_internal.h"
+#include "rocksdb/db.h"
+#include "rocksdb/types.h"
+#include "test_util/testutil.h"
+#include "util/gflags_compat.h"
+
+// Run a thread to perform Put's.
+// Another thread uses GetUpdatesSince API to keep getting the updates.
+// options :
+// --num_inserts = the num of inserts the first thread should perform.
+// --wal_ttl = the wal ttl for the run.
+
+DEFINE_uint64(num_inserts, 1000,
+ "the num of inserts the first thread should"
+ " perform.");
+DEFINE_uint64(wal_ttl_seconds, 1000, "the wal ttl for the run(in seconds)");
+DEFINE_uint64(wal_size_limit_MB, 10,
+ "the wal size limit for the run"
+ "(in MB)");
+
+using ROCKSDB_NAMESPACE::BatchResult;
+using ROCKSDB_NAMESPACE::DB;
+using ROCKSDB_NAMESPACE::DestroyDB;
+using ROCKSDB_NAMESPACE::Env;
+using ROCKSDB_NAMESPACE::Options;
+using ROCKSDB_NAMESPACE::Random;
+using ROCKSDB_NAMESPACE::SequenceNumber;
+using ROCKSDB_NAMESPACE::Slice;
+using ROCKSDB_NAMESPACE::Status;
+using ROCKSDB_NAMESPACE::TransactionLogIterator;
+using ROCKSDB_NAMESPACE::WriteOptions;
+
+using GFLAGS_NAMESPACE::ParseCommandLineFlags;
+using GFLAGS_NAMESPACE::SetUsageMessage;
+
+struct DataPumpThread {
+ DB* db; // Assumption DB is Open'ed already.
+};
+
+static void DataPumpThreadBody(void* arg) {
+ DataPumpThread* t = reinterpret_cast<DataPumpThread*>(arg);
+ DB* db = t->db;
+ Random rnd(301);
+ uint64_t i = 0;
+ while (i++ < FLAGS_num_inserts) {
+ if (!db->Put(WriteOptions(), Slice(rnd.RandomString(500)),
+ Slice(rnd.RandomString(500)))
+ .ok()) {
+ fprintf(stderr, "Error in put\n");
+ exit(1);
+ }
+ }
+}
+
+int main(int argc, const char** argv) {
+ SetUsageMessage(
+ std::string("\nUSAGE:\n") + std::string(argv[0]) +
+ " --num_inserts=<num_inserts> --wal_ttl_seconds=<WAL_ttl_seconds>" +
+ " --wal_size_limit_MB=<WAL_size_limit_MB>");
+ ParseCommandLineFlags(&argc, const_cast<char***>(&argv), true);
+
+ Env* env = Env::Default();
+ std::string default_db_path;
+ env->GetTestDirectory(&default_db_path);
+ default_db_path += "db_repl_stress";
+ Options options;
+ options.create_if_missing = true;
+ options.WAL_ttl_seconds = FLAGS_wal_ttl_seconds;
+ options.WAL_size_limit_MB = FLAGS_wal_size_limit_MB;
+ DB* db;
+ DestroyDB(default_db_path, options);
+
+ Status s = DB::Open(options, default_db_path, &db);
+
+ if (!s.ok()) {
+ fprintf(stderr, "Could not open DB due to %s\n", s.ToString().c_str());
+ exit(1);
+ }
+
+ DataPumpThread dataPump;
+ dataPump.db = db;
+ env->StartThread(DataPumpThreadBody, &dataPump);
+
+ std::unique_ptr<TransactionLogIterator> iter;
+ SequenceNumber currentSeqNum = 1;
+ uint64_t num_read = 0;
+ for (;;) {
+ iter.reset();
+ // Continue to probe a bit more after all received
+ size_t probes = 0;
+ while (!db->GetUpdatesSince(currentSeqNum, &iter).ok()) {
+ probes++;
+ if (probes > 100 && num_read >= FLAGS_num_inserts) {
+ if (num_read > FLAGS_num_inserts) {
+ fprintf(stderr, "Too many updates read: %ld expected: %ld\n",
+ (long)num_read, (long)FLAGS_num_inserts);
+ exit(1);
+ }
+ fprintf(stderr, "Successful!\n");
+ return 0;
+ }
+ }
+ fprintf(stderr, "Refreshing iterator\n");
+ for (; iter->Valid(); iter->Next(), num_read++, currentSeqNum++) {
+ BatchResult res = iter->GetBatch();
+ if (res.sequence != currentSeqNum) {
+ fprintf(stderr, "Missed a seq no. b/w %ld and %ld\n",
+ (long)currentSeqNum, (long)res.sequence);
+ exit(1);
+ }
+ }
+ }
+}
+
+#endif // GFLAGS
+
+#else // ROCKSDB_LITE
+#include <stdio.h>
+int main(int /*argc*/, char** /*argv*/) {
+ fprintf(stderr, "Not supported in lite mode.\n");
+ return 1;
+}
+#endif // ROCKSDB_LITE
diff --git a/src/rocksdb/tools/db_sanity_test.cc b/src/rocksdb/tools/db_sanity_test.cc
new file mode 100644
index 000000000..8cc67f5d5
--- /dev/null
+++ b/src/rocksdb/tools/db_sanity_test.cc
@@ -0,0 +1,300 @@
+// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+// This source code is licensed under both the GPLv2 (found in the
+// COPYING file in the root directory) and Apache 2.0 License
+// (found in the LICENSE.Apache file in the root directory).
+
+#include <cstdio>
+#include <cstdlib>
+#include <memory>
+#include <vector>
+
+#include "port/port.h"
+#include "rocksdb/comparator.h"
+#include "rocksdb/db.h"
+#include "rocksdb/env.h"
+#include "rocksdb/filter_policy.h"
+#include "rocksdb/options.h"
+#include "rocksdb/slice.h"
+#include "rocksdb/slice_transform.h"
+#include "rocksdb/status.h"
+#include "rocksdb/table.h"
+#include "util/string_util.h"
+
+namespace ROCKSDB_NAMESPACE {
+
+class SanityTest {
+ public:
+ explicit SanityTest(const std::string& path)
+ : env_(Env::Default()), path_(path) {
+ env_->CreateDirIfMissing(path);
+ }
+ virtual ~SanityTest() {}
+
+ virtual std::string Name() const = 0;
+ virtual Options GetOptions() const = 0;
+
+ Status Create() {
+ Options options = GetOptions();
+ options.create_if_missing = true;
+ std::string dbname = path_ + Name();
+ Status s = DestroyDB(dbname, options);
+ if (!s.ok()) {
+ return s;
+ }
+ DB* db = nullptr;
+ s = DB::Open(options, dbname, &db);
+ std::unique_ptr<DB> db_guard(db);
+ if (!s.ok()) {
+ return s;
+ }
+ for (int i = 0; i < 1000000; ++i) {
+ std::string k = "key" + std::to_string(i);
+ std::string v = "value" + std::to_string(i);
+ s = db->Put(WriteOptions(), Slice(k), Slice(v));
+ if (!s.ok()) {
+ return s;
+ }
+ }
+ return db->Flush(FlushOptions());
+ }
+ Status Verify() {
+ DB* db = nullptr;
+ std::string dbname = path_ + Name();
+ Status s = DB::Open(GetOptions(), dbname, &db);
+ std::unique_ptr<DB> db_guard(db);
+ if (!s.ok()) {
+ return s;
+ }
+ for (int i = 0; i < 1000000; ++i) {
+ std::string k = "key" + std::to_string(i);
+ std::string v = "value" + std::to_string(i);
+ std::string result;
+ s = db->Get(ReadOptions(), Slice(k), &result);
+ if (!s.ok()) {
+ return s;
+ }
+ if (result != v) {
+ return Status::Corruption("Unexpected value for key " + k);
+ }
+ }
+ return Status::OK();
+ }
+
+ private:
+ Env* env_;
+ std::string const path_;
+};
+
+class SanityTestBasic : public SanityTest {
+ public:
+ explicit SanityTestBasic(const std::string& path) : SanityTest(path) {}
+ virtual Options GetOptions() const override {
+ Options options;
+ options.create_if_missing = true;
+ return options;
+ }
+ virtual std::string Name() const override { return "Basic"; }
+};
+
+class SanityTestSpecialComparator : public SanityTest {
+ public:
+ explicit SanityTestSpecialComparator(const std::string& path)
+ : SanityTest(path) {
+ options_.comparator = new NewComparator();
+ }
+ ~SanityTestSpecialComparator() { delete options_.comparator; }
+ virtual Options GetOptions() const override { return options_; }
+ virtual std::string Name() const override { return "SpecialComparator"; }
+
+ private:
+ class NewComparator : public Comparator {
+ public:
+ virtual const char* Name() const override {
+ return "rocksdb.NewComparator";
+ }
+ virtual int Compare(const Slice& a, const Slice& b) const override {
+ return BytewiseComparator()->Compare(a, b);
+ }
+ virtual void FindShortestSeparator(std::string* s,
+ const Slice& l) const override {
+ BytewiseComparator()->FindShortestSeparator(s, l);
+ }
+ virtual void FindShortSuccessor(std::string* key) const override {
+ BytewiseComparator()->FindShortSuccessor(key);
+ }
+ };
+ Options options_;
+};
+
+class SanityTestZlibCompression : public SanityTest {
+ public:
+ explicit SanityTestZlibCompression(const std::string& path)
+ : SanityTest(path) {
+ options_.compression = kZlibCompression;
+ }
+ virtual Options GetOptions() const override { return options_; }
+ virtual std::string Name() const override { return "ZlibCompression"; }
+
+ private:
+ Options options_;
+};
+
+class SanityTestZlibCompressionVersion2 : public SanityTest {
+ public:
+ explicit SanityTestZlibCompressionVersion2(const std::string& path)
+ : SanityTest(path) {
+ options_.compression = kZlibCompression;
+ BlockBasedTableOptions table_options;
+#if ROCKSDB_MAJOR > 3 || (ROCKSDB_MAJOR == 3 && ROCKSDB_MINOR >= 10)
+ table_options.format_version = 2;
+#endif
+ options_.table_factory.reset(NewBlockBasedTableFactory(table_options));
+ }
+ virtual Options GetOptions() const override { return options_; }
+ virtual std::string Name() const override {
+ return "ZlibCompressionVersion2";
+ }
+
+ private:
+ Options options_;
+};
+
+class SanityTestLZ4Compression : public SanityTest {
+ public:
+ explicit SanityTestLZ4Compression(const std::string& path)
+ : SanityTest(path) {
+ options_.compression = kLZ4Compression;
+ }
+ virtual Options GetOptions() const override { return options_; }
+ virtual std::string Name() const override { return "LZ4Compression"; }
+
+ private:
+ Options options_;
+};
+
+class SanityTestLZ4HCCompression : public SanityTest {
+ public:
+ explicit SanityTestLZ4HCCompression(const std::string& path)
+ : SanityTest(path) {
+ options_.compression = kLZ4HCCompression;
+ }
+ virtual Options GetOptions() const override { return options_; }
+ virtual std::string Name() const override { return "LZ4HCCompression"; }
+
+ private:
+ Options options_;
+};
+
+class SanityTestZSTDCompression : public SanityTest {
+ public:
+ explicit SanityTestZSTDCompression(const std::string& path)
+ : SanityTest(path) {
+ options_.compression = kZSTD;
+ }
+ virtual Options GetOptions() const override { return options_; }
+ virtual std::string Name() const override { return "ZSTDCompression"; }
+
+ private:
+ Options options_;
+};
+
+#ifndef ROCKSDB_LITE
+class SanityTestPlainTableFactory : public SanityTest {
+ public:
+ explicit SanityTestPlainTableFactory(const std::string& path)
+ : SanityTest(path) {
+ options_.table_factory.reset(NewPlainTableFactory());
+ options_.prefix_extractor.reset(NewFixedPrefixTransform(2));
+ options_.allow_mmap_reads = true;
+ }
+ ~SanityTestPlainTableFactory() {}
+ virtual Options GetOptions() const override { return options_; }
+ virtual std::string Name() const override { return "PlainTable"; }
+
+ private:
+ Options options_;
+};
+#endif // ROCKSDB_LITE
+
+class SanityTestBloomFilter : public SanityTest {
+ public:
+ explicit SanityTestBloomFilter(const std::string& path) : SanityTest(path) {
+ BlockBasedTableOptions table_options;
+ table_options.filter_policy.reset(NewBloomFilterPolicy(10));
+ options_.table_factory.reset(NewBlockBasedTableFactory(table_options));
+ }
+ ~SanityTestBloomFilter() {}
+ virtual Options GetOptions() const override { return options_; }
+ virtual std::string Name() const override { return "BloomFilter"; }
+
+ private:
+ Options options_;
+};
+
+namespace {
+bool RunSanityTests(const std::string& command, const std::string& path) {
+ bool result = true;
+// Suppress false positive clang static anaylzer warnings.
+#ifndef __clang_analyzer__
+ std::vector<SanityTest*> sanity_tests = {
+ new SanityTestBasic(path),
+ new SanityTestSpecialComparator(path),
+ new SanityTestZlibCompression(path),
+ new SanityTestZlibCompressionVersion2(path),
+ new SanityTestLZ4Compression(path),
+ new SanityTestLZ4HCCompression(path),
+ new SanityTestZSTDCompression(path),
+#ifndef ROCKSDB_LITE
+ new SanityTestPlainTableFactory(path),
+#endif // ROCKSDB_LITE
+ new SanityTestBloomFilter(path)};
+
+ if (command == "create") {
+ fprintf(stderr, "Creating...\n");
+ } else {
+ fprintf(stderr, "Verifying...\n");
+ }
+ for (auto sanity_test : sanity_tests) {
+ Status s;
+ fprintf(stderr, "%s -- ", sanity_test->Name().c_str());
+ if (command == "create") {
+ s = sanity_test->Create();
+ } else {
+ assert(command == "verify");
+ s = sanity_test->Verify();
+ }
+ fprintf(stderr, "%s\n", s.ToString().c_str());
+ if (!s.ok()) {
+ fprintf(stderr, "FAIL\n");
+ result = false;
+ }
+
+ delete sanity_test;
+ }
+#endif // __clang_analyzer__
+ return result;
+}
+} // namespace
+
+} // namespace ROCKSDB_NAMESPACE
+
+int main(int argc, char** argv) {
+ std::string path, command;
+ bool ok = (argc == 3);
+ if (ok) {
+ path = std::string(argv[1]);
+ command = std::string(argv[2]);
+ ok = (command == "create" || command == "verify");
+ }
+ if (!ok) {
+ fprintf(stderr, "Usage: %s <path> [create|verify] \n", argv[0]);
+ exit(1);
+ }
+ if (path.back() != '/') {
+ path += "/";
+ }
+
+ bool sanity_ok = ROCKSDB_NAMESPACE::RunSanityTests(command, path);
+
+ return sanity_ok ? 0 : 1;
+}
diff --git a/src/rocksdb/tools/dbench_monitor b/src/rocksdb/tools/dbench_monitor
new file mode 100755
index 000000000..d85f9d070
--- /dev/null
+++ b/src/rocksdb/tools/dbench_monitor
@@ -0,0 +1,102 @@
+#!/usr/bin/env bash
+#
+#(c) 2004-present, Facebook Inc. All rights reserved.
+#
+#see LICENSE file for more information on use/redistribution rights.
+#
+
+#
+#dbench_monitor: monitor db_bench process for violation of memory utilization
+#
+#default usage will monitor 'virtual memory size'. See below for standard options
+#passed to db_bench during this test.
+#
+# See also: ./pflag for the actual monitoring script that does the work
+#
+#NOTE:
+# You may end up with some /tmp/ files if db_bench OR
+# this script OR ./pflag was killed unceremoniously
+#
+# If you see the script taking a long time, trying "kill"
+# will usually cleanly exit.
+#
+#
+DIR=`dirname $0`
+LOG=/tmp/`basename $0`.$$
+DB_BENCH="$DIR/../db_bench";
+PFLAG=${DIR}/pflag
+
+usage() {
+ cat <<HELP; exit
+
+Usage: $0 [-h]
+
+-h: prints this help message
+
+This program will run the db_bench script to monitor memory usage
+using the 'pflag' program. It launches db_bench with default settings
+for certain arguments. You can change the defaults passed to
+'db_bench' program, by setting the following environment
+variables:
+
+ bs [block_size]
+ ztype [compression_type]
+ benches [benchmarks]
+ reads [reads]
+ threads [threads]
+ cs [cache_size]
+ vsize [value_size]
+ comp [compression_ratio]
+ num [num]
+
+See the code for more info
+
+HELP
+
+}
+
+[ ! -x ${DB_BENCH} ] && echo "WARNING: ${DB_BENCH} doesn't exist, abort!" && exit -1;
+
+[ "x$1" = "x-h" ] && usage;
+
+trap 'rm -f ${LOG}; kill ${PID}; echo "Interrupted, exiting";' 1 2 3 15
+
+touch $LOG;
+
+: ${bs:=16384}
+: ${ztype:=zlib}
+: ${benches:=readwhilewriting}
+: ${reads:=$((1*1024*1024))};
+: ${threads:=8}
+: ${vsize:=2000}
+: ${comp:=0.5}
+: ${num:=10000}
+: ${cs:=$((1*1024*1024*1024))};
+
+DEBUG=1 #Set to 0 to remove chattiness
+
+
+if [ "x$DEBUG" != "x" ]; then
+ #
+ #NOTE: under some circumstances, --use_existing_db may leave LOCK files under ${TMPDIR}/rocksdb/*
+ #cleanup the dir and re-run
+ #
+ echo DEBUG: Will run $DB_BENCH --block_size=$bs --compression_type=$ztype --benchmarks="$benches" --reads="$reads" --threads="$threads" --cache_size=$cs --value_size=$vsize --compression_ratio=$comp --num=$num --use_existing_db
+
+fi
+
+$DB_BENCH --block_size=$bs --compression_type=$ztype --benchmarks="$benches" --reads="$reads" --threads="$threads" --cache_size=$cs --value_size=$vsize --compression_ratio=$comp --num=$num --use_existing_db >$LOG 2>&1 &
+
+if [ $? -ne 0 ]; then
+ warn "WARNING: ${DB_BENCH} did not launch successfully! Abort!";
+ exit;
+fi
+PID=$!
+
+#
+#Start the monitoring. Default is "vsz" monitoring for upto cache_size ($cs) value of virtual mem
+#You could also monitor RSS and CPUTIME (bsdtime). Try 'pflag -h' for how to do this
+#
+${PFLAG} -p $PID -v
+
+rm -f $LOG;
diff --git a/src/rocksdb/tools/dump/db_dump_tool.cc b/src/rocksdb/tools/dump/db_dump_tool.cc
new file mode 100644
index 000000000..427a54d99
--- /dev/null
+++ b/src/rocksdb/tools/dump/db_dump_tool.cc
@@ -0,0 +1,260 @@
+// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+// This source code is licensed under both the GPLv2 (found in the
+// COPYING file in the root directory) and Apache 2.0 License
+// (found in the LICENSE.Apache file in the root directory).
+
+#ifndef ROCKSDB_LITE
+
+#include "rocksdb/db_dump_tool.h"
+
+#include <cinttypes>
+#include <iostream>
+
+#include "rocksdb/db.h"
+#include "rocksdb/env.h"
+#include "util/coding.h"
+
+namespace ROCKSDB_NAMESPACE {
+
+bool DbDumpTool::Run(const DumpOptions& dump_options,
+ ROCKSDB_NAMESPACE::Options options) {
+ ROCKSDB_NAMESPACE::DB* dbptr;
+ ROCKSDB_NAMESPACE::Status status;
+ std::unique_ptr<ROCKSDB_NAMESPACE::WritableFile> dumpfile;
+ char hostname[1024];
+ int64_t timesec = 0;
+ std::string abspath;
+ char json[4096];
+
+ static const char* magicstr = "ROCKDUMP";
+ static const char versionstr[8] = {0, 0, 0, 0, 0, 0, 0, 1};
+
+ ROCKSDB_NAMESPACE::Env* env = ROCKSDB_NAMESPACE::Env::Default();
+
+ // Open the database
+ options.create_if_missing = false;
+ status = ROCKSDB_NAMESPACE::DB::OpenForReadOnly(options, dump_options.db_path,
+ &dbptr);
+ if (!status.ok()) {
+ std::cerr << "Unable to open database '" << dump_options.db_path
+ << "' for reading: " << status.ToString() << std::endl;
+ return false;
+ }
+
+ const std::unique_ptr<ROCKSDB_NAMESPACE::DB> db(dbptr);
+
+ status = env->NewWritableFile(dump_options.dump_location, &dumpfile,
+ ROCKSDB_NAMESPACE::EnvOptions());
+ if (!status.ok()) {
+ std::cerr << "Unable to open dump file '" << dump_options.dump_location
+ << "' for writing: " << status.ToString() << std::endl;
+ return false;
+ }
+
+ ROCKSDB_NAMESPACE::Slice magicslice(magicstr, 8);
+ status = dumpfile->Append(magicslice);
+ if (!status.ok()) {
+ std::cerr << "Append failed: " << status.ToString() << std::endl;
+ return false;
+ }
+
+ ROCKSDB_NAMESPACE::Slice versionslice(versionstr, 8);
+ status = dumpfile->Append(versionslice);
+ if (!status.ok()) {
+ std::cerr << "Append failed: " << status.ToString() << std::endl;
+ return false;
+ }
+
+ if (dump_options.anonymous) {
+ snprintf(json, sizeof(json), "{}");
+ } else {
+ status = env->GetHostName(hostname, sizeof(hostname));
+ status = env->GetCurrentTime(&timesec);
+ status = env->GetAbsolutePath(dump_options.db_path, &abspath);
+ snprintf(json, sizeof(json),
+ "{ \"database-path\": \"%s\", \"hostname\": \"%s\", "
+ "\"creation-time\": %" PRIi64 " }",
+ abspath.c_str(), hostname, timesec);
+ }
+
+ ROCKSDB_NAMESPACE::Slice infoslice(json, strlen(json));
+ char infosize[4];
+ ROCKSDB_NAMESPACE::EncodeFixed32(infosize, (uint32_t)infoslice.size());
+ ROCKSDB_NAMESPACE::Slice infosizeslice(infosize, 4);
+ status = dumpfile->Append(infosizeslice);
+ if (!status.ok()) {
+ std::cerr << "Append failed: " << status.ToString() << std::endl;
+ return false;
+ }
+ status = dumpfile->Append(infoslice);
+ if (!status.ok()) {
+ std::cerr << "Append failed: " << status.ToString() << std::endl;
+ return false;
+ }
+
+ const std::unique_ptr<ROCKSDB_NAMESPACE::Iterator> it(
+ db->NewIterator(ROCKSDB_NAMESPACE::ReadOptions()));
+ for (it->SeekToFirst(); it->Valid(); it->Next()) {
+ char keysize[4];
+ ROCKSDB_NAMESPACE::EncodeFixed32(keysize, (uint32_t)it->key().size());
+ ROCKSDB_NAMESPACE::Slice keysizeslice(keysize, 4);
+ status = dumpfile->Append(keysizeslice);
+ if (!status.ok()) {
+ std::cerr << "Append failed: " << status.ToString() << std::endl;
+ return false;
+ }
+ status = dumpfile->Append(it->key());
+ if (!status.ok()) {
+ std::cerr << "Append failed: " << status.ToString() << std::endl;
+ return false;
+ }
+
+ char valsize[4];
+ ROCKSDB_NAMESPACE::EncodeFixed32(valsize, (uint32_t)it->value().size());
+ ROCKSDB_NAMESPACE::Slice valsizeslice(valsize, 4);
+ status = dumpfile->Append(valsizeslice);
+ if (!status.ok()) {
+ std::cerr << "Append failed: " << status.ToString() << std::endl;
+ return false;
+ }
+ status = dumpfile->Append(it->value());
+ if (!status.ok()) {
+ std::cerr << "Append failed: " << status.ToString() << std::endl;
+ return false;
+ }
+ }
+ if (!it->status().ok()) {
+ std::cerr << "Database iteration failed: " << status.ToString()
+ << std::endl;
+ return false;
+ }
+ return true;
+}
+
+bool DbUndumpTool::Run(const UndumpOptions& undump_options,
+ ROCKSDB_NAMESPACE::Options options) {
+ ROCKSDB_NAMESPACE::DB* dbptr;
+ ROCKSDB_NAMESPACE::Status status;
+ ROCKSDB_NAMESPACE::Env* env;
+ std::unique_ptr<ROCKSDB_NAMESPACE::SequentialFile> dumpfile;
+ ROCKSDB_NAMESPACE::Slice slice;
+ char scratch8[8];
+
+ static const char* magicstr = "ROCKDUMP";
+ static const char versionstr[8] = {0, 0, 0, 0, 0, 0, 0, 1};
+
+ env = ROCKSDB_NAMESPACE::Env::Default();
+
+ status = env->NewSequentialFile(undump_options.dump_location, &dumpfile,
+ ROCKSDB_NAMESPACE::EnvOptions());
+ if (!status.ok()) {
+ std::cerr << "Unable to open dump file '" << undump_options.dump_location
+ << "' for reading: " << status.ToString() << std::endl;
+ return false;
+ }
+
+ status = dumpfile->Read(8, &slice, scratch8);
+ if (!status.ok() || slice.size() != 8 ||
+ memcmp(slice.data(), magicstr, 8) != 0) {
+ std::cerr << "File '" << undump_options.dump_location
+ << "' is not a recognizable dump file." << std::endl;
+ return false;
+ }
+
+ status = dumpfile->Read(8, &slice, scratch8);
+ if (!status.ok() || slice.size() != 8 ||
+ memcmp(slice.data(), versionstr, 8) != 0) {
+ std::cerr << "File '" << undump_options.dump_location
+ << "' version not recognized." << std::endl;
+ return false;
+ }
+
+ status = dumpfile->Read(4, &slice, scratch8);
+ if (!status.ok() || slice.size() != 4) {
+ std::cerr << "Unable to read info blob size." << std::endl;
+ return false;
+ }
+ uint32_t infosize = ROCKSDB_NAMESPACE::DecodeFixed32(slice.data());
+ status = dumpfile->Skip(infosize);
+ if (!status.ok()) {
+ std::cerr << "Unable to skip info blob: " << status.ToString() << std::endl;
+ return false;
+ }
+
+ options.create_if_missing = true;
+ status = ROCKSDB_NAMESPACE::DB::Open(options, undump_options.db_path, &dbptr);
+ if (!status.ok()) {
+ std::cerr << "Unable to open database '" << undump_options.db_path
+ << "' for writing: " << status.ToString() << std::endl;
+ return false;
+ }
+
+ const std::unique_ptr<ROCKSDB_NAMESPACE::DB> db(dbptr);
+
+ uint32_t last_keysize = 64;
+ size_t last_valsize = 1 << 20;
+ std::unique_ptr<char[]> keyscratch(new char[last_keysize]);
+ std::unique_ptr<char[]> valscratch(new char[last_valsize]);
+
+ while (1) {
+ uint32_t keysize, valsize;
+ ROCKSDB_NAMESPACE::Slice keyslice;
+ ROCKSDB_NAMESPACE::Slice valslice;
+
+ status = dumpfile->Read(4, &slice, scratch8);
+ if (!status.ok() || slice.size() != 4) break;
+ keysize = ROCKSDB_NAMESPACE::DecodeFixed32(slice.data());
+ if (keysize > last_keysize) {
+ while (keysize > last_keysize) last_keysize *= 2;
+ keyscratch = std::unique_ptr<char[]>(new char[last_keysize]);
+ }
+
+ status = dumpfile->Read(keysize, &keyslice, keyscratch.get());
+ if (!status.ok() || keyslice.size() != keysize) {
+ std::cerr << "Key read failure: "
+ << (status.ok() ? "insufficient data" : status.ToString())
+ << std::endl;
+ return false;
+ }
+
+ status = dumpfile->Read(4, &slice, scratch8);
+ if (!status.ok() || slice.size() != 4) {
+ std::cerr << "Unable to read value size: "
+ << (status.ok() ? "insufficient data" : status.ToString())
+ << std::endl;
+ return false;
+ }
+ valsize = ROCKSDB_NAMESPACE::DecodeFixed32(slice.data());
+ if (valsize > last_valsize) {
+ while (valsize > last_valsize) last_valsize *= 2;
+ valscratch = std::unique_ptr<char[]>(new char[last_valsize]);
+ }
+
+ status = dumpfile->Read(valsize, &valslice, valscratch.get());
+ if (!status.ok() || valslice.size() != valsize) {
+ std::cerr << "Unable to read value: "
+ << (status.ok() ? "insufficient data" : status.ToString())
+ << std::endl;
+ return false;
+ }
+
+ status = db->Put(ROCKSDB_NAMESPACE::WriteOptions(), keyslice, valslice);
+ if (!status.ok()) {
+ fprintf(stderr, "Unable to write database entry\n");
+ return false;
+ }
+ }
+
+ if (undump_options.compact_db) {
+ status = db->CompactRange(ROCKSDB_NAMESPACE::CompactRangeOptions(), nullptr,
+ nullptr);
+ if (!status.ok()) {
+ fprintf(stderr,
+ "Unable to compact the database after loading the dumped file\n");
+ return false;
+ }
+ }
+ return true;
+}
+} // namespace ROCKSDB_NAMESPACE
+#endif // ROCKSDB_LITE
diff --git a/src/rocksdb/tools/dump/rocksdb_dump.cc b/src/rocksdb/tools/dump/rocksdb_dump.cc
new file mode 100644
index 000000000..358457e92
--- /dev/null
+++ b/src/rocksdb/tools/dump/rocksdb_dump.cc
@@ -0,0 +1,63 @@
+// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+// This source code is licensed under both the GPLv2 (found in the
+// COPYING file in the root directory) and Apache 2.0 License
+// (found in the LICENSE.Apache file in the root directory).
+
+#if !(defined GFLAGS) || defined(ROCKSDB_LITE)
+
+#include <cstdio>
+int main() {
+#ifndef GFLAGS
+ fprintf(stderr, "Please install gflags to run rocksdb tools\n");
+#endif
+#ifdef ROCKSDB_LITE
+ fprintf(stderr, "DbDumpTool is not supported in ROCKSDB_LITE\n");
+#endif
+ return 1;
+}
+
+#else
+
+#include "rocksdb/convenience.h"
+#include "rocksdb/db_dump_tool.h"
+#include "util/gflags_compat.h"
+
+DEFINE_string(db_path, "", "Path to the db that will be dumped");
+DEFINE_string(dump_location, "", "Path to where the dump file location");
+DEFINE_bool(anonymous, false,
+ "Remove information like db path, creation time from dumped file");
+DEFINE_string(db_options, "",
+ "Options string used to open the database that will be dumped");
+
+int main(int argc, char** argv) {
+ GFLAGS_NAMESPACE::ParseCommandLineFlags(&argc, &argv, true);
+
+ if (FLAGS_db_path == "" || FLAGS_dump_location == "") {
+ fprintf(stderr, "Please set --db_path and --dump_location\n");
+ return 1;
+ }
+
+ ROCKSDB_NAMESPACE::DumpOptions dump_options;
+ dump_options.db_path = FLAGS_db_path;
+ dump_options.dump_location = FLAGS_dump_location;
+ dump_options.anonymous = FLAGS_anonymous;
+
+ ROCKSDB_NAMESPACE::Options db_options;
+ if (FLAGS_db_options != "") {
+ ROCKSDB_NAMESPACE::Options parsed_options;
+ ROCKSDB_NAMESPACE::Status s = ROCKSDB_NAMESPACE::GetOptionsFromString(
+ db_options, FLAGS_db_options, &parsed_options);
+ if (!s.ok()) {
+ fprintf(stderr, "Cannot parse provided db_options\n");
+ return 1;
+ }
+ db_options = parsed_options;
+ }
+
+ ROCKSDB_NAMESPACE::DbDumpTool tool;
+ if (!tool.Run(dump_options, db_options)) {
+ return 1;
+ }
+ return 0;
+}
+#endif // !(defined GFLAGS) || defined(ROCKSDB_LITE)
diff --git a/src/rocksdb/tools/dump/rocksdb_undump.cc b/src/rocksdb/tools/dump/rocksdb_undump.cc
new file mode 100644
index 000000000..2ff128548
--- /dev/null
+++ b/src/rocksdb/tools/dump/rocksdb_undump.cc
@@ -0,0 +1,62 @@
+// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+// This source code is licensed under both the GPLv2 (found in the
+// COPYING file in the root directory) and Apache 2.0 License
+// (found in the LICENSE.Apache file in the root directory).
+
+#if !(defined GFLAGS) || defined(ROCKSDB_LITE)
+
+#include <cstdio>
+int main() {
+#ifndef GFLAGS
+ fprintf(stderr, "Please install gflags to run rocksdb tools\n");
+#endif
+#ifdef ROCKSDB_LITE
+ fprintf(stderr, "DbUndumpTool is not supported in ROCKSDB_LITE\n");
+#endif
+ return 1;
+}
+
+#else
+
+#include "rocksdb/convenience.h"
+#include "rocksdb/db_dump_tool.h"
+#include "util/gflags_compat.h"
+
+DEFINE_string(dump_location, "", "Path to the dump file that will be loaded");
+DEFINE_string(db_path, "", "Path to the db that we will undump the file into");
+DEFINE_bool(compact, false, "Compact the db after loading the dumped file");
+DEFINE_string(db_options, "",
+ "Options string used to open the database that will be loaded");
+
+int main(int argc, char **argv) {
+ GFLAGS_NAMESPACE::ParseCommandLineFlags(&argc, &argv, true);
+
+ if (FLAGS_db_path == "" || FLAGS_dump_location == "") {
+ fprintf(stderr, "Please set --db_path and --dump_location\n");
+ return 1;
+ }
+
+ ROCKSDB_NAMESPACE::UndumpOptions undump_options;
+ undump_options.db_path = FLAGS_db_path;
+ undump_options.dump_location = FLAGS_dump_location;
+ undump_options.compact_db = FLAGS_compact;
+
+ ROCKSDB_NAMESPACE::Options db_options;
+ if (FLAGS_db_options != "") {
+ ROCKSDB_NAMESPACE::Options parsed_options;
+ ROCKSDB_NAMESPACE::Status s = ROCKSDB_NAMESPACE::GetOptionsFromString(
+ db_options, FLAGS_db_options, &parsed_options);
+ if (!s.ok()) {
+ fprintf(stderr, "Cannot parse provided db_options\n");
+ return 1;
+ }
+ db_options = parsed_options;
+ }
+
+ ROCKSDB_NAMESPACE::DbUndumpTool tool;
+ if (!tool.Run(undump_options, db_options)) {
+ return 1;
+ }
+ return 0;
+}
+#endif // !(defined GFLAGS) || defined(ROCKSDB_LITE)
diff --git a/src/rocksdb/tools/generate_random_db.sh b/src/rocksdb/tools/generate_random_db.sh
new file mode 100755
index 000000000..5b5962617
--- /dev/null
+++ b/src/rocksdb/tools/generate_random_db.sh
@@ -0,0 +1,31 @@
+#!/usr/bin/env bash
+# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
+#
+# A shell script to load some pre generated data file to a DB using ldb tool
+# ./ldb needs to be avaible to be executed.
+#
+# Usage: <SCRIPT> <input_data_path> <DB Path>
+
+if [ "$#" -lt 2 ]; then
+ echo "usage: $BASH_SOURCE <input_data_path> <DB Path>"
+ exit 1
+fi
+
+input_data_dir=$1
+db_dir=$2
+rm -rf $db_dir
+
+echo == Loading data from $input_data_dir to $db_dir
+
+declare -a compression_opts=("no" "snappy" "zlib" "bzip2")
+
+set -e
+
+n=0
+
+for f in `ls -1 $input_data_dir`
+do
+ echo == Loading $f with compression ${compression_opts[n % 4]}
+ ./ldb load --db=$db_dir --compression_type=${compression_opts[n % 4]} --bloom_bits=10 --auto_compaction=false --create_if_missing < $input_data_dir/$f
+ let "n = n + 1"
+done
diff --git a/src/rocksdb/tools/ingest_external_sst.sh b/src/rocksdb/tools/ingest_external_sst.sh
new file mode 100755
index 000000000..8e2fed7ce
--- /dev/null
+++ b/src/rocksdb/tools/ingest_external_sst.sh
@@ -0,0 +1,18 @@
+#!/usr/bin/env bash
+# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
+#
+#
+
+if [ "$#" -lt 2 ]; then
+ echo "usage: $BASH_SOURCE <DB Path> <External SST Dir>"
+ exit 1
+fi
+
+db_dir=$1
+external_sst_dir=$2
+
+for f in `find $external_sst_dir -name extern_sst*`
+do
+ echo == Ingesting external SST file $f to DB at $db_dir
+ ./ldb --db=$db_dir --create_if_missing ingest_extern_sst $f
+done
diff --git a/src/rocksdb/tools/io_tracer_parser.cc b/src/rocksdb/tools/io_tracer_parser.cc
new file mode 100644
index 000000000..41ef45d97
--- /dev/null
+++ b/src/rocksdb/tools/io_tracer_parser.cc
@@ -0,0 +1,25 @@
+// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+// This source code is licensed under both the GPLv2 (found in the
+// COPYING file in the root directory) and Apache 2.0 License
+// (found in the LICENSE.Apache file in the root directory).
+//
+#ifndef ROCKSDB_LITE
+#ifndef GFLAGS
+#include <cstdio>
+int main() {
+ fprintf(stderr, "Please install gflags to run rocksdb tools\n");
+ return 1;
+}
+#else // GFLAGS
+#include "tools/io_tracer_parser_tool.h"
+int main(int argc, char** argv) {
+ return ROCKSDB_NAMESPACE::io_tracer_parser(argc, argv);
+}
+#endif // GFLAGS
+#else // ROCKSDB_LITE
+#include <stdio.h>
+int main(int /*argc*/, char** /*argv*/) {
+ fprintf(stderr, "Not supported in lite mode.\n");
+ return 1;
+}
+#endif // ROCKSDB_LITE
diff --git a/src/rocksdb/tools/io_tracer_parser_test.cc b/src/rocksdb/tools/io_tracer_parser_test.cc
new file mode 100644
index 000000000..41be5fa96
--- /dev/null
+++ b/src/rocksdb/tools/io_tracer_parser_test.cc
@@ -0,0 +1,190 @@
+// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+// This source code is licensed under both the GPLv2 (found in the
+// COPYING file in the root directory) and Apache 2.0 License
+// (found in the LICENSE.Apache file in the root directory).
+//
+
+#ifndef ROCKSDB_LITE
+#ifndef GFLAGS
+#include <cstdio>
+int main() {
+ fprintf(stderr, "Please install gflags to run io_tracer_parser_test\n");
+ return 0;
+}
+#else
+
+#include <string>
+#include <vector>
+
+#include "rocksdb/db.h"
+#include "rocksdb/env.h"
+#include "rocksdb/status.h"
+#include "rocksdb/trace_reader_writer.h"
+#include "test_util/testharness.h"
+#include "test_util/testutil.h"
+#include "tools/io_tracer_parser_tool.h"
+
+namespace ROCKSDB_NAMESPACE {
+
+namespace {
+const int kMaxArgCount = 100;
+const size_t kArgBufferSize = 100000;
+} // namespace
+
+class IOTracerParserTest : public testing::Test {
+ public:
+ IOTracerParserTest() {
+ test_path_ = test::PerThreadDBPath("io_tracer_parser_test");
+ env_ = ROCKSDB_NAMESPACE::Env::Default();
+ EXPECT_OK(env_->CreateDirIfMissing(test_path_));
+ trace_file_path_ = test_path_ + "/io_trace_file";
+ dbname_ = test_path_ + "/db";
+ Options options;
+ options.create_if_missing = true;
+ EXPECT_OK(DB::Open(options, dbname_, &db_));
+ }
+
+ ~IOTracerParserTest() {
+ if (env_->FileExists(trace_file_path_).ok()) {
+ EXPECT_OK(env_->DeleteFile(trace_file_path_));
+ }
+ if (db_ != nullptr) {
+ Options options;
+ options.env = env_;
+ delete db_;
+ db_ = nullptr;
+ EXPECT_OK(DestroyDB(dbname_, options));
+ }
+ EXPECT_OK(env_->DeleteDir(test_path_));
+ }
+
+ void GenerateIOTrace() {
+ WriteOptions write_opt;
+ TraceOptions trace_opt;
+ std::unique_ptr<TraceWriter> trace_writer;
+
+ ASSERT_OK(NewFileTraceWriter(env_, env_options_, trace_file_path_,
+ &trace_writer));
+
+ ASSERT_OK(db_->StartIOTrace(trace_opt, std::move(trace_writer)));
+
+ for (int i = 0; i < 10; i++) {
+ ASSERT_OK(db_->Put(write_opt, "key_" + std::to_string(i),
+ "value_" + std::to_string(i)));
+ ASSERT_OK(db_->Flush(FlushOptions()));
+ }
+
+ ASSERT_OK(db_->EndIOTrace());
+ ASSERT_OK(env_->FileExists(trace_file_path_));
+ }
+
+ void RunIOTracerParserTool() {
+ std::vector<std::string> params = {"./io_tracer_parser",
+ "-io_trace_file=" + trace_file_path_};
+
+ char arg_buffer[kArgBufferSize];
+ char* argv[kMaxArgCount];
+ int argc = 0;
+ int cursor = 0;
+ for (const auto& arg : params) {
+ ASSERT_LE(cursor + arg.size() + 1, kArgBufferSize);
+ ASSERT_LE(argc + 1, kMaxArgCount);
+
+ snprintf(arg_buffer + cursor, arg.size() + 1, "%s", arg.c_str());
+
+ argv[argc++] = arg_buffer + cursor;
+ cursor += static_cast<int>(arg.size()) + 1;
+ }
+ ASSERT_EQ(0, ROCKSDB_NAMESPACE::io_tracer_parser(argc, argv));
+ }
+
+ DB* db_;
+ Env* env_;
+ EnvOptions env_options_;
+ std::string trace_file_path_;
+ std::string output_file_;
+ std::string test_path_;
+ std::string dbname_;
+};
+
+TEST_F(IOTracerParserTest, InvalidArguments) {
+ {
+ std::vector<std::string> params = {"./io_tracer_parser"};
+ char arg_buffer[kArgBufferSize];
+ char* argv[kMaxArgCount];
+ int argc = 0;
+ int cursor = 0;
+ for (const auto& arg : params) {
+ ASSERT_LE(cursor + arg.size() + 1, kArgBufferSize);
+ ASSERT_LE(argc + 1, kMaxArgCount);
+
+ snprintf(arg_buffer + cursor, arg.size() + 1, "%s", arg.c_str());
+
+ argv[argc++] = arg_buffer + cursor;
+ cursor += static_cast<int>(arg.size()) + 1;
+ }
+ ASSERT_EQ(1, ROCKSDB_NAMESPACE::io_tracer_parser(argc, argv));
+ }
+}
+
+TEST_F(IOTracerParserTest, DumpAndParseIOTraceRecords) {
+ GenerateIOTrace();
+ RunIOTracerParserTool();
+}
+
+TEST_F(IOTracerParserTest, NoRecordingAfterEndIOTrace) {
+ uint64_t file_size = 0;
+ // Generate IO trace records and parse them.
+ {
+ GenerateIOTrace();
+ RunIOTracerParserTool();
+ ASSERT_OK(env_->GetFileSize(trace_file_path_, &file_size));
+ }
+ // Once DB::EndIOTrace is invoked in GenerateIOTrace(), no new records should
+ // be appended.
+ {
+ WriteOptions write_opt;
+ for (int i = 10; i < 20; i++) {
+ ASSERT_OK(db_->Put(write_opt, "key_" + std::to_string(i),
+ "value_" + std::to_string(i)));
+ ASSERT_OK(db_->Flush(FlushOptions()));
+ }
+ }
+
+ uint64_t new_file_size = 0;
+ ASSERT_OK(env_->GetFileSize(trace_file_path_, &new_file_size));
+ ASSERT_EQ(file_size, new_file_size);
+}
+
+TEST_F(IOTracerParserTest, NoRecordingBeforeStartIOTrace) {
+ {
+ WriteOptions write_opt;
+ for (int i = 10; i < 20; i++) {
+ ASSERT_OK(db_->Put(write_opt, "key_" + std::to_string(i),
+ "value_" + std::to_string(i)));
+ ASSERT_OK(db_->Flush(FlushOptions()));
+ }
+ // IO trace file doesn't exist
+ ASSERT_NOK(env_->FileExists(trace_file_path_));
+ }
+ // Generate IO trace records and parse them.
+ {
+ GenerateIOTrace();
+ RunIOTracerParserTool();
+ }
+}
+} // namespace ROCKSDB_NAMESPACE
+
+int main(int argc, char** argv) {
+ ROCKSDB_NAMESPACE::port::InstallStackTraceHandler();
+ ::testing::InitGoogleTest(&argc, argv);
+ return RUN_ALL_TESTS();
+}
+#endif // GFLAGS
+#else
+#include <stdio.h>
+int main(int /*argc*/, char** /*argv*/) {
+ fprintf(stderr, "io_tracer_parser_test is not supported in ROCKSDB_LITE\n");
+ return 0;
+}
+#endif // ROCKSDB_LITE
diff --git a/src/rocksdb/tools/io_tracer_parser_tool.cc b/src/rocksdb/tools/io_tracer_parser_tool.cc
new file mode 100644
index 000000000..01b920f3b
--- /dev/null
+++ b/src/rocksdb/tools/io_tracer_parser_tool.cc
@@ -0,0 +1,144 @@
+// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+// This source code is licensed under both the GPLv2 (found in the
+// COPYING file in the root directory) and Apache 2.0 License
+// (found in the LICENSE.Apache file in the root directory).
+
+#ifndef ROCKSDB_LITE
+#ifdef GFLAGS
+#include "tools/io_tracer_parser_tool.h"
+
+#include <cinttypes>
+#include <cstdio>
+#include <iomanip>
+#include <memory>
+#include <sstream>
+
+#include "port/lang.h"
+#include "rocksdb/trace_reader_writer.h"
+#include "trace_replay/io_tracer.h"
+#include "util/gflags_compat.h"
+
+using GFLAGS_NAMESPACE::ParseCommandLineFlags;
+
+DEFINE_string(io_trace_file, "", "The IO trace file path.");
+
+namespace ROCKSDB_NAMESPACE {
+
+IOTraceRecordParser::IOTraceRecordParser(const std::string& input_file)
+ : input_file_(input_file) {}
+
+void IOTraceRecordParser::PrintHumanReadableHeader(
+ const IOTraceHeader& header) {
+ std::stringstream ss;
+ ss << "Start Time: " << header.start_time
+ << "\nRocksDB Major Version: " << header.rocksdb_major_version
+ << "\nRocksDB Minor Version: " << header.rocksdb_minor_version << "\n";
+ fprintf(stdout, "%s", ss.str().c_str());
+}
+
+void IOTraceRecordParser::PrintHumanReadableIOTraceRecord(
+ const IOTraceRecord& record) {
+ std::stringstream ss;
+ ss << "Access Time : " << std::setw(20) << std::left
+ << record.access_timestamp << ", File Name: " << std::setw(20) << std::left
+ << record.file_name.c_str() << ", File Operation: " << std::setw(18)
+ << std::left << record.file_operation.c_str()
+ << ", Latency: " << std::setw(10) << std::left << record.latency
+ << ", IO Status: " << record.io_status.c_str();
+
+ // Each bit in io_op_data stores which corresponding info from IOTraceOp will
+ // be added in the trace. Foreg, if bit at position 1 is set then
+ // IOTraceOp::kIOLen (length) will be logged in the record (Since
+ // IOTraceOp::kIOLen = 1 in the enum). So find all the set positions in
+ // io_op_data one by one and, update corresponsing info in the trace record,
+ // unset that bit to find other set bits until io_op_data = 0.
+ /* Read remaining options based on io_op_data set by file operation */
+ int64_t io_op_data = static_cast<int64_t>(record.io_op_data);
+ while (io_op_data) {
+ // Find the rightmost set bit.
+ uint32_t set_pos = static_cast<uint32_t>(log2(io_op_data & -io_op_data));
+ switch (set_pos) {
+ case IOTraceOp::kIOFileSize:
+ ss << ", File Size: " << record.file_size;
+ break;
+ case IOTraceOp::kIOLen:
+ ss << ", Length: " << record.len;
+ break;
+ case IOTraceOp::kIOOffset:
+ ss << ", Offset: " << record.offset;
+ break;
+ default:
+ assert(false);
+ }
+ // unset the rightmost bit.
+ io_op_data &= (io_op_data - 1);
+ }
+
+ int64_t trace_data = static_cast<int64_t>(record.trace_data);
+ while (trace_data) {
+ // Find the rightmost set bit.
+ uint32_t set_pos = static_cast<uint32_t>(log2(trace_data & -trace_data));
+ switch (set_pos) {
+ case IODebugContext::TraceData::kRequestID:
+ ss << ", Request Id: " << record.request_id;
+ break;
+ default:
+ assert(false);
+ }
+ // unset the rightmost bit.
+ trace_data &= (trace_data - 1);
+ }
+
+ ss << "\n";
+ fprintf(stdout, "%s", ss.str().c_str());
+}
+
+int IOTraceRecordParser::ReadIOTraceRecords() {
+ Status status;
+ Env* env(Env::Default());
+ std::unique_ptr<TraceReader> trace_reader;
+ std::unique_ptr<IOTraceReader> io_trace_reader;
+
+ status = NewFileTraceReader(env, EnvOptions(), input_file_, &trace_reader);
+ if (!status.ok()) {
+ fprintf(stderr, "%s: %s\n", input_file_.c_str(), status.ToString().c_str());
+ return 1;
+ }
+ io_trace_reader.reset(new IOTraceReader(std::move(trace_reader)));
+
+ // Read the header and dump it in a file.
+ IOTraceHeader header;
+ status = io_trace_reader->ReadHeader(&header);
+ if (!status.ok()) {
+ fprintf(stderr, "%s: %s\n", input_file_.c_str(), status.ToString().c_str());
+ return 1;
+ }
+ PrintHumanReadableHeader(header);
+
+ // Read the records one by one and print them in human readable format.
+ while (status.ok()) {
+ IOTraceRecord record;
+ status = io_trace_reader->ReadIOOp(&record);
+ if (!status.ok()) {
+ break;
+ }
+ PrintHumanReadableIOTraceRecord(record);
+ }
+ return 0;
+}
+
+int io_tracer_parser(int argc, char** argv) {
+ ParseCommandLineFlags(&argc, &argv, true);
+
+ if (FLAGS_io_trace_file.empty()) {
+ fprintf(stderr, "IO Trace file path is empty\n");
+ return 1;
+ }
+
+ IOTraceRecordParser io_tracer_parser(FLAGS_io_trace_file);
+ return io_tracer_parser.ReadIOTraceRecords();
+}
+
+} // namespace ROCKSDB_NAMESPACE
+#endif // GFLAGS
+#endif // ROCKSDB_LITE
diff --git a/src/rocksdb/tools/io_tracer_parser_tool.h b/src/rocksdb/tools/io_tracer_parser_tool.h
new file mode 100644
index 000000000..6c22c8f89
--- /dev/null
+++ b/src/rocksdb/tools/io_tracer_parser_tool.h
@@ -0,0 +1,40 @@
+// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+// This source code is licensed under both the GPLv2 (found in the
+// COPYING file in the root directory) and Apache 2.0 License
+// (found in the LICENSE.Apache file in the root directory).
+
+#ifndef ROCKSDB_LITE
+#pragma once
+
+#include <memory>
+
+#include "rocksdb/env.h"
+#include "rocksdb/status.h"
+
+namespace ROCKSDB_NAMESPACE {
+
+struct IOTraceHeader;
+struct IOTraceRecord;
+
+// IOTraceRecordParser class reads the IO trace file (in binary format) and
+// dumps the human readable records in output_file_.
+class IOTraceRecordParser {
+ public:
+ explicit IOTraceRecordParser(const std::string& input_file);
+
+ // ReadIOTraceRecords reads the binary trace file records one by one and
+ // invoke PrintHumanReadableIOTraceRecord to dump the records in output_file_.
+ int ReadIOTraceRecords();
+
+ private:
+ void PrintHumanReadableHeader(const IOTraceHeader& header);
+ void PrintHumanReadableIOTraceRecord(const IOTraceRecord& record);
+
+ // Binary file that contains IO trace records.
+ std::string input_file_;
+};
+
+int io_tracer_parser(int argc, char** argv);
+
+} // namespace ROCKSDB_NAMESPACE
+#endif // ROCKSDB_LITE
diff --git a/src/rocksdb/tools/ldb.cc b/src/rocksdb/tools/ldb.cc
new file mode 100644
index 000000000..482383be8
--- /dev/null
+++ b/src/rocksdb/tools/ldb.cc
@@ -0,0 +1,21 @@
+// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+// This source code is licensed under both the GPLv2 (found in the
+// COPYING file in the root directory) and Apache 2.0 License
+// (found in the LICENSE.Apache file in the root directory).
+//
+#ifndef ROCKSDB_LITE
+
+#include "rocksdb/ldb_tool.h"
+
+int main(int argc, char** argv) {
+ ROCKSDB_NAMESPACE::LDBTool tool;
+ tool.Run(argc, argv);
+ return 0;
+}
+#else
+#include <stdio.h>
+int main(int /*argc*/, char** /*argv*/) {
+ fprintf(stderr, "Not supported in lite mode.\n");
+ return 1;
+}
+#endif // ROCKSDB_LITE
diff --git a/src/rocksdb/tools/ldb_cmd.cc b/src/rocksdb/tools/ldb_cmd.cc
new file mode 100644
index 000000000..ecd2d2977
--- /dev/null
+++ b/src/rocksdb/tools/ldb_cmd.cc
@@ -0,0 +1,4263 @@
+
+// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+// This source code is licensed under both the GPLv2 (found in the
+// COPYING file in the root directory) and Apache 2.0 License
+// (found in the LICENSE.Apache file in the root directory).
+//
+#ifndef ROCKSDB_LITE
+#include "rocksdb/utilities/ldb_cmd.h"
+
+#include <cinttypes>
+#include <cstdlib>
+#include <ctime>
+#include <fstream>
+#include <functional>
+#include <iostream>
+#include <limits>
+#include <sstream>
+#include <stdexcept>
+#include <string>
+
+#include "db/blob/blob_index.h"
+#include "db/db_impl/db_impl.h"
+#include "db/dbformat.h"
+#include "db/log_reader.h"
+#include "db/version_util.h"
+#include "db/write_batch_internal.h"
+#include "file/filename.h"
+#include "rocksdb/cache.h"
+#include "rocksdb/experimental.h"
+#include "rocksdb/file_checksum.h"
+#include "rocksdb/filter_policy.h"
+#include "rocksdb/options.h"
+#include "rocksdb/table_properties.h"
+#include "rocksdb/utilities/backup_engine.h"
+#include "rocksdb/utilities/checkpoint.h"
+#include "rocksdb/utilities/debug.h"
+#include "rocksdb/utilities/options_util.h"
+#include "rocksdb/write_batch.h"
+#include "rocksdb/write_buffer_manager.h"
+#include "table/scoped_arena_iterator.h"
+#include "table/sst_file_dumper.h"
+#include "tools/ldb_cmd_impl.h"
+#include "util/cast_util.h"
+#include "util/coding.h"
+#include "util/file_checksum_helper.h"
+#include "util/stderr_logger.h"
+#include "util/string_util.h"
+#include "utilities/blob_db/blob_dump_tool.h"
+#include "utilities/merge_operators.h"
+#include "utilities/ttl/db_ttl_impl.h"
+
+namespace ROCKSDB_NAMESPACE {
+
+class FileChecksumGenCrc32c;
+class FileChecksumGenCrc32cFactory;
+
+const std::string LDBCommand::ARG_ENV_URI = "env_uri";
+const std::string LDBCommand::ARG_FS_URI = "fs_uri";
+const std::string LDBCommand::ARG_DB = "db";
+const std::string LDBCommand::ARG_PATH = "path";
+const std::string LDBCommand::ARG_SECONDARY_PATH = "secondary_path";
+const std::string LDBCommand::ARG_HEX = "hex";
+const std::string LDBCommand::ARG_KEY_HEX = "key_hex";
+const std::string LDBCommand::ARG_VALUE_HEX = "value_hex";
+const std::string LDBCommand::ARG_CF_NAME = "column_family";
+const std::string LDBCommand::ARG_TTL = "ttl";
+const std::string LDBCommand::ARG_TTL_START = "start_time";
+const std::string LDBCommand::ARG_TTL_END = "end_time";
+const std::string LDBCommand::ARG_TIMESTAMP = "timestamp";
+const std::string LDBCommand::ARG_TRY_LOAD_OPTIONS = "try_load_options";
+const std::string LDBCommand::ARG_DISABLE_CONSISTENCY_CHECKS =
+ "disable_consistency_checks";
+const std::string LDBCommand::ARG_IGNORE_UNKNOWN_OPTIONS =
+ "ignore_unknown_options";
+const std::string LDBCommand::ARG_FROM = "from";
+const std::string LDBCommand::ARG_TO = "to";
+const std::string LDBCommand::ARG_MAX_KEYS = "max_keys";
+const std::string LDBCommand::ARG_BLOOM_BITS = "bloom_bits";
+const std::string LDBCommand::ARG_FIX_PREFIX_LEN = "fix_prefix_len";
+const std::string LDBCommand::ARG_COMPRESSION_TYPE = "compression_type";
+const std::string LDBCommand::ARG_COMPRESSION_MAX_DICT_BYTES =
+ "compression_max_dict_bytes";
+const std::string LDBCommand::ARG_BLOCK_SIZE = "block_size";
+const std::string LDBCommand::ARG_AUTO_COMPACTION = "auto_compaction";
+const std::string LDBCommand::ARG_DB_WRITE_BUFFER_SIZE = "db_write_buffer_size";
+const std::string LDBCommand::ARG_WRITE_BUFFER_SIZE = "write_buffer_size";
+const std::string LDBCommand::ARG_FILE_SIZE = "file_size";
+const std::string LDBCommand::ARG_CREATE_IF_MISSING = "create_if_missing";
+const std::string LDBCommand::ARG_NO_VALUE = "no_value";
+const std::string LDBCommand::ARG_ENABLE_BLOB_FILES = "enable_blob_files";
+const std::string LDBCommand::ARG_MIN_BLOB_SIZE = "min_blob_size";
+const std::string LDBCommand::ARG_BLOB_FILE_SIZE = "blob_file_size";
+const std::string LDBCommand::ARG_BLOB_COMPRESSION_TYPE =
+ "blob_compression_type";
+const std::string LDBCommand::ARG_ENABLE_BLOB_GARBAGE_COLLECTION =
+ "enable_blob_garbage_collection";
+const std::string LDBCommand::ARG_BLOB_GARBAGE_COLLECTION_AGE_CUTOFF =
+ "blob_garbage_collection_age_cutoff";
+const std::string LDBCommand::ARG_BLOB_GARBAGE_COLLECTION_FORCE_THRESHOLD =
+ "blob_garbage_collection_force_threshold";
+const std::string LDBCommand::ARG_BLOB_COMPACTION_READAHEAD_SIZE =
+ "blob_compaction_readahead_size";
+const std::string LDBCommand::ARG_BLOB_FILE_STARTING_LEVEL =
+ "blob_file_starting_level";
+const std::string LDBCommand::ARG_PREPOPULATE_BLOB_CACHE =
+ "prepopulate_blob_cache";
+const std::string LDBCommand::ARG_DECODE_BLOB_INDEX = "decode_blob_index";
+const std::string LDBCommand::ARG_DUMP_UNCOMPRESSED_BLOBS =
+ "dump_uncompressed_blobs";
+
+const char* LDBCommand::DELIM = " ==> ";
+
+namespace {
+
+void DumpWalFile(Options options, std::string wal_file, bool print_header,
+ bool print_values, bool is_write_committed,
+ LDBCommandExecuteResult* exec_state);
+
+void DumpSstFile(Options options, std::string filename, bool output_hex,
+ bool show_properties, bool decode_blob_index,
+ std::string from_key = "", std::string to_key = "");
+
+void DumpBlobFile(const std::string& filename, bool is_key_hex,
+ bool is_value_hex, bool dump_uncompressed_blobs);
+}; // namespace
+
+LDBCommand* LDBCommand::InitFromCmdLineArgs(
+ int argc, char const* const* argv, const Options& options,
+ const LDBOptions& ldb_options,
+ const std::vector<ColumnFamilyDescriptor>* column_families) {
+ std::vector<std::string> args;
+ for (int i = 1; i < argc; i++) {
+ args.push_back(argv[i]);
+ }
+ return InitFromCmdLineArgs(args, options, ldb_options, column_families,
+ SelectCommand);
+}
+
+/**
+ * Parse the command-line arguments and create the appropriate LDBCommand2
+ * instance.
+ * The command line arguments must be in the following format:
+ * ./ldb --db=PATH_TO_DB [--commonOpt1=commonOpt1Val] ..
+ * COMMAND <PARAM1> <PARAM2> ... [-cmdSpecificOpt1=cmdSpecificOpt1Val] ..
+ * This is similar to the command line format used by HBaseClientTool.
+ * Command name is not included in args.
+ * Returns nullptr if the command-line cannot be parsed.
+ */
+LDBCommand* LDBCommand::InitFromCmdLineArgs(
+ const std::vector<std::string>& args, const Options& options,
+ const LDBOptions& ldb_options,
+ const std::vector<ColumnFamilyDescriptor>* /*column_families*/,
+ const std::function<LDBCommand*(const ParsedParams&)>& selector) {
+ // --x=y command line arguments are added as x->y map entries in
+ // parsed_params.option_map.
+ //
+ // Command-line arguments of the form --hex end up in this array as hex to
+ // parsed_params.flags
+ ParsedParams parsed_params;
+
+ // Everything other than option_map and flags. Represents commands
+ // and their parameters. For eg: put key1 value1 go into this vector.
+ std::vector<std::string> cmdTokens;
+
+ const std::string OPTION_PREFIX = "--";
+
+ for (const auto& arg : args) {
+ if (arg[0] == '-' && arg[1] == '-') {
+ std::vector<std::string> splits = StringSplit(arg, '=');
+ // --option_name=option_value
+ if (splits.size() == 2) {
+ std::string optionKey = splits[0].substr(OPTION_PREFIX.size());
+ parsed_params.option_map[optionKey] = splits[1];
+ } else if (splits.size() == 1) {
+ // --flag_name
+ std::string optionKey = splits[0].substr(OPTION_PREFIX.size());
+ parsed_params.flags.push_back(optionKey);
+ } else {
+ // --option_name=option_value, option_value contains '='
+ std::string optionKey = splits[0].substr(OPTION_PREFIX.size());
+ parsed_params.option_map[optionKey] =
+ arg.substr(splits[0].length() + 1);
+ }
+ } else {
+ cmdTokens.push_back(arg);
+ }
+ }
+
+ if (cmdTokens.size() < 1) {
+ fprintf(stderr, "Command not specified!");
+ return nullptr;
+ }
+
+ parsed_params.cmd = cmdTokens[0];
+ parsed_params.cmd_params.assign(cmdTokens.begin() + 1, cmdTokens.end());
+
+ LDBCommand* command = selector(parsed_params);
+
+ if (command) {
+ command->SetDBOptions(options);
+ command->SetLDBOptions(ldb_options);
+ }
+ return command;
+}
+
+LDBCommand* LDBCommand::SelectCommand(const ParsedParams& parsed_params) {
+ if (parsed_params.cmd == GetCommand::Name()) {
+ return new GetCommand(parsed_params.cmd_params, parsed_params.option_map,
+ parsed_params.flags);
+ } else if (parsed_params.cmd == PutCommand::Name()) {
+ return new PutCommand(parsed_params.cmd_params, parsed_params.option_map,
+ parsed_params.flags);
+ } else if (parsed_params.cmd == BatchPutCommand::Name()) {
+ return new BatchPutCommand(parsed_params.cmd_params,
+ parsed_params.option_map, parsed_params.flags);
+ } else if (parsed_params.cmd == ScanCommand::Name()) {
+ return new ScanCommand(parsed_params.cmd_params, parsed_params.option_map,
+ parsed_params.flags);
+ } else if (parsed_params.cmd == DeleteCommand::Name()) {
+ return new DeleteCommand(parsed_params.cmd_params, parsed_params.option_map,
+ parsed_params.flags);
+ } else if (parsed_params.cmd == SingleDeleteCommand::Name()) {
+ return new SingleDeleteCommand(parsed_params.cmd_params,
+ parsed_params.option_map,
+ parsed_params.flags);
+ } else if (parsed_params.cmd == DeleteRangeCommand::Name()) {
+ return new DeleteRangeCommand(parsed_params.cmd_params,
+ parsed_params.option_map,
+ parsed_params.flags);
+ } else if (parsed_params.cmd == ApproxSizeCommand::Name()) {
+ return new ApproxSizeCommand(parsed_params.cmd_params,
+ parsed_params.option_map, parsed_params.flags);
+ } else if (parsed_params.cmd == DBQuerierCommand::Name()) {
+ return new DBQuerierCommand(parsed_params.cmd_params,
+ parsed_params.option_map, parsed_params.flags);
+ } else if (parsed_params.cmd == CompactorCommand::Name()) {
+ return new CompactorCommand(parsed_params.cmd_params,
+ parsed_params.option_map, parsed_params.flags);
+ } else if (parsed_params.cmd == WALDumperCommand::Name()) {
+ return new WALDumperCommand(parsed_params.cmd_params,
+ parsed_params.option_map, parsed_params.flags);
+ } else if (parsed_params.cmd == ReduceDBLevelsCommand::Name()) {
+ return new ReduceDBLevelsCommand(parsed_params.cmd_params,
+ parsed_params.option_map,
+ parsed_params.flags);
+ } else if (parsed_params.cmd == ChangeCompactionStyleCommand::Name()) {
+ return new ChangeCompactionStyleCommand(parsed_params.cmd_params,
+ parsed_params.option_map,
+ parsed_params.flags);
+ } else if (parsed_params.cmd == DBDumperCommand::Name()) {
+ return new DBDumperCommand(parsed_params.cmd_params,
+ parsed_params.option_map, parsed_params.flags);
+ } else if (parsed_params.cmd == DBLoaderCommand::Name()) {
+ return new DBLoaderCommand(parsed_params.cmd_params,
+ parsed_params.option_map, parsed_params.flags);
+ } else if (parsed_params.cmd == ManifestDumpCommand::Name()) {
+ return new ManifestDumpCommand(parsed_params.cmd_params,
+ parsed_params.option_map,
+ parsed_params.flags);
+ } else if (parsed_params.cmd == FileChecksumDumpCommand::Name()) {
+ return new FileChecksumDumpCommand(parsed_params.cmd_params,
+ parsed_params.option_map,
+ parsed_params.flags);
+ } else if (parsed_params.cmd == GetPropertyCommand::Name()) {
+ return new GetPropertyCommand(parsed_params.cmd_params,
+ parsed_params.option_map,
+ parsed_params.flags);
+ } else if (parsed_params.cmd == ListColumnFamiliesCommand::Name()) {
+ return new ListColumnFamiliesCommand(parsed_params.cmd_params,
+ parsed_params.option_map,
+ parsed_params.flags);
+ } else if (parsed_params.cmd == CreateColumnFamilyCommand::Name()) {
+ return new CreateColumnFamilyCommand(parsed_params.cmd_params,
+ parsed_params.option_map,
+ parsed_params.flags);
+ } else if (parsed_params.cmd == DropColumnFamilyCommand::Name()) {
+ return new DropColumnFamilyCommand(parsed_params.cmd_params,
+ parsed_params.option_map,
+ parsed_params.flags);
+ } else if (parsed_params.cmd == DBFileDumperCommand::Name()) {
+ return new DBFileDumperCommand(parsed_params.cmd_params,
+ parsed_params.option_map,
+ parsed_params.flags);
+ } else if (parsed_params.cmd == DBLiveFilesMetadataDumperCommand::Name()) {
+ return new DBLiveFilesMetadataDumperCommand(parsed_params.cmd_params,
+ parsed_params.option_map,
+ parsed_params.flags);
+ } else if (parsed_params.cmd == InternalDumpCommand::Name()) {
+ return new InternalDumpCommand(parsed_params.cmd_params,
+ parsed_params.option_map,
+ parsed_params.flags);
+ } else if (parsed_params.cmd == CheckConsistencyCommand::Name()) {
+ return new CheckConsistencyCommand(parsed_params.cmd_params,
+ parsed_params.option_map,
+ parsed_params.flags);
+ } else if (parsed_params.cmd == CheckPointCommand::Name()) {
+ return new CheckPointCommand(parsed_params.cmd_params,
+ parsed_params.option_map, parsed_params.flags);
+ } else if (parsed_params.cmd == RepairCommand::Name()) {
+ return new RepairCommand(parsed_params.cmd_params, parsed_params.option_map,
+ parsed_params.flags);
+ } else if (parsed_params.cmd == BackupCommand::Name()) {
+ return new BackupCommand(parsed_params.cmd_params, parsed_params.option_map,
+ parsed_params.flags);
+ } else if (parsed_params.cmd == RestoreCommand::Name()) {
+ return new RestoreCommand(parsed_params.cmd_params,
+ parsed_params.option_map, parsed_params.flags);
+ } else if (parsed_params.cmd == WriteExternalSstFilesCommand::Name()) {
+ return new WriteExternalSstFilesCommand(parsed_params.cmd_params,
+ parsed_params.option_map,
+ parsed_params.flags);
+ } else if (parsed_params.cmd == IngestExternalSstFilesCommand::Name()) {
+ return new IngestExternalSstFilesCommand(parsed_params.cmd_params,
+ parsed_params.option_map,
+ parsed_params.flags);
+ } else if (parsed_params.cmd == ListFileRangeDeletesCommand::Name()) {
+ return new ListFileRangeDeletesCommand(parsed_params.option_map,
+ parsed_params.flags);
+ } else if (parsed_params.cmd == UnsafeRemoveSstFileCommand::Name()) {
+ return new UnsafeRemoveSstFileCommand(parsed_params.cmd_params,
+ parsed_params.option_map,
+ parsed_params.flags);
+ } else if (parsed_params.cmd == UpdateManifestCommand::Name()) {
+ return new UpdateManifestCommand(parsed_params.cmd_params,
+ parsed_params.option_map,
+ parsed_params.flags);
+ }
+ return nullptr;
+}
+
+/* Run the command, and return the execute result. */
+void LDBCommand::Run() {
+ if (!exec_state_.IsNotStarted()) {
+ return;
+ }
+
+ if (!options_.env || options_.env == Env::Default()) {
+ Env* env = Env::Default();
+ Status s = Env::CreateFromUri(config_options_, env_uri_, fs_uri_, &env,
+ &env_guard_);
+ if (!s.ok()) {
+ fprintf(stderr, "%s\n", s.ToString().c_str());
+ exec_state_ = LDBCommandExecuteResult::Failed(s.ToString());
+ return;
+ }
+ options_.env = env;
+ }
+
+ if (db_ == nullptr && !NoDBOpen()) {
+ OpenDB();
+ if (exec_state_.IsFailed() && try_load_options_) {
+ // We don't always return if there is a failure because a WAL file or
+ // manifest file can be given to "dump" command so we should continue.
+ // --try_load_options is not valid in those cases.
+ return;
+ }
+ }
+
+ // We'll intentionally proceed even if the DB can't be opened because users
+ // can also specify a filename, not just a directory.
+ DoCommand();
+
+ if (exec_state_.IsNotStarted()) {
+ exec_state_ = LDBCommandExecuteResult::Succeed("");
+ }
+
+ if (db_ != nullptr) {
+ CloseDB();
+ }
+}
+
+LDBCommand::LDBCommand(const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags, bool is_read_only,
+ const std::vector<std::string>& valid_cmd_line_options)
+ : db_(nullptr),
+ db_ttl_(nullptr),
+ is_read_only_(is_read_only),
+ is_key_hex_(false),
+ is_value_hex_(false),
+ is_db_ttl_(false),
+ timestamp_(false),
+ try_load_options_(false),
+ create_if_missing_(false),
+ option_map_(options),
+ flags_(flags),
+ valid_cmd_line_options_(valid_cmd_line_options) {
+ auto itr = options.find(ARG_DB);
+ if (itr != options.end()) {
+ db_path_ = itr->second;
+ }
+
+ itr = options.find(ARG_ENV_URI);
+ if (itr != options.end()) {
+ env_uri_ = itr->second;
+ }
+
+ itr = options.find(ARG_FS_URI);
+ if (itr != options.end()) {
+ fs_uri_ = itr->second;
+ }
+
+ itr = options.find(ARG_CF_NAME);
+ if (itr != options.end()) {
+ column_family_name_ = itr->second;
+ } else {
+ column_family_name_ = kDefaultColumnFamilyName;
+ }
+
+ itr = options.find(ARG_SECONDARY_PATH);
+ secondary_path_ = "";
+ if (itr != options.end()) {
+ secondary_path_ = itr->second;
+ }
+
+ is_key_hex_ = IsKeyHex(options, flags);
+ is_value_hex_ = IsValueHex(options, flags);
+ is_db_ttl_ = IsFlagPresent(flags, ARG_TTL);
+ timestamp_ = IsFlagPresent(flags, ARG_TIMESTAMP);
+ try_load_options_ = IsTryLoadOptions(options, flags);
+ force_consistency_checks_ =
+ !IsFlagPresent(flags, ARG_DISABLE_CONSISTENCY_CHECKS);
+ enable_blob_files_ = IsFlagPresent(flags, ARG_ENABLE_BLOB_FILES);
+ enable_blob_garbage_collection_ =
+ IsFlagPresent(flags, ARG_ENABLE_BLOB_GARBAGE_COLLECTION);
+ config_options_.ignore_unknown_options =
+ IsFlagPresent(flags, ARG_IGNORE_UNKNOWN_OPTIONS);
+}
+
+void LDBCommand::OpenDB() {
+ PrepareOptions();
+ if (!exec_state_.IsNotStarted()) {
+ return;
+ }
+ if (column_families_.empty() && !options_.merge_operator) {
+ // No harm to add a general merge operator if it is not specified.
+ options_.merge_operator = MergeOperators::CreateStringAppendOperator(':');
+ }
+ // Open the DB.
+ Status st;
+ std::vector<ColumnFamilyHandle*> handles_opened;
+ if (is_db_ttl_) {
+ // ldb doesn't yet support TTL DB with multiple column families
+ if (!column_family_name_.empty() || !column_families_.empty()) {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ "ldb doesn't support TTL DB with multiple column families");
+ }
+ if (!secondary_path_.empty()) {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ "Open as secondary is not supported for TTL DB yet.");
+ }
+ if (is_read_only_) {
+ st = DBWithTTL::Open(options_, db_path_, &db_ttl_, 0, true);
+ } else {
+ st = DBWithTTL::Open(options_, db_path_, &db_ttl_);
+ }
+ db_ = db_ttl_;
+ } else {
+ if (is_read_only_ && secondary_path_.empty()) {
+ if (column_families_.empty()) {
+ st = DB::OpenForReadOnly(options_, db_path_, &db_);
+ } else {
+ st = DB::OpenForReadOnly(options_, db_path_, column_families_,
+ &handles_opened, &db_);
+ }
+ } else {
+ if (column_families_.empty()) {
+ if (secondary_path_.empty()) {
+ st = DB::Open(options_, db_path_, &db_);
+ } else {
+ st = DB::OpenAsSecondary(options_, db_path_, secondary_path_, &db_);
+ }
+ } else {
+ if (secondary_path_.empty()) {
+ st = DB::Open(options_, db_path_, column_families_, &handles_opened,
+ &db_);
+ } else {
+ st = DB::OpenAsSecondary(options_, db_path_, secondary_path_,
+ column_families_, &handles_opened, &db_);
+ }
+ }
+ }
+ }
+ if (!st.ok()) {
+ std::string msg = st.ToString();
+ exec_state_ = LDBCommandExecuteResult::Failed(msg);
+ } else if (!handles_opened.empty()) {
+ assert(handles_opened.size() == column_families_.size());
+ bool found_cf_name = false;
+ for (size_t i = 0; i < handles_opened.size(); i++) {
+ cf_handles_[column_families_[i].name] = handles_opened[i];
+ if (column_family_name_ == column_families_[i].name) {
+ found_cf_name = true;
+ }
+ }
+ if (!found_cf_name) {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ "Non-existing column family " + column_family_name_);
+ CloseDB();
+ }
+ } else {
+ // We successfully opened DB in single column family mode.
+ assert(column_families_.empty());
+ if (column_family_name_ != kDefaultColumnFamilyName) {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ "Non-existing column family " + column_family_name_);
+ CloseDB();
+ }
+ }
+}
+
+void LDBCommand::CloseDB() {
+ if (db_ != nullptr) {
+ for (auto& pair : cf_handles_) {
+ delete pair.second;
+ }
+ Status s = db_->Close();
+ s.PermitUncheckedError();
+ delete db_;
+ db_ = nullptr;
+ }
+}
+
+ColumnFamilyHandle* LDBCommand::GetCfHandle() {
+ if (!cf_handles_.empty()) {
+ auto it = cf_handles_.find(column_family_name_);
+ if (it == cf_handles_.end()) {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ "Cannot find column family " + column_family_name_);
+ } else {
+ return it->second;
+ }
+ }
+ return db_->DefaultColumnFamily();
+}
+
+std::vector<std::string> LDBCommand::BuildCmdLineOptions(
+ std::vector<std::string> options) {
+ std::vector<std::string> ret = {ARG_ENV_URI,
+ ARG_FS_URI,
+ ARG_DB,
+ ARG_SECONDARY_PATH,
+ ARG_BLOOM_BITS,
+ ARG_BLOCK_SIZE,
+ ARG_AUTO_COMPACTION,
+ ARG_COMPRESSION_TYPE,
+ ARG_COMPRESSION_MAX_DICT_BYTES,
+ ARG_WRITE_BUFFER_SIZE,
+ ARG_FILE_SIZE,
+ ARG_FIX_PREFIX_LEN,
+ ARG_TRY_LOAD_OPTIONS,
+ ARG_DISABLE_CONSISTENCY_CHECKS,
+ ARG_ENABLE_BLOB_FILES,
+ ARG_MIN_BLOB_SIZE,
+ ARG_BLOB_FILE_SIZE,
+ ARG_BLOB_COMPRESSION_TYPE,
+ ARG_ENABLE_BLOB_GARBAGE_COLLECTION,
+ ARG_BLOB_GARBAGE_COLLECTION_AGE_CUTOFF,
+ ARG_BLOB_GARBAGE_COLLECTION_FORCE_THRESHOLD,
+ ARG_BLOB_COMPACTION_READAHEAD_SIZE,
+ ARG_BLOB_FILE_STARTING_LEVEL,
+ ARG_PREPOPULATE_BLOB_CACHE,
+ ARG_IGNORE_UNKNOWN_OPTIONS,
+ ARG_CF_NAME};
+ ret.insert(ret.end(), options.begin(), options.end());
+ return ret;
+}
+
+/**
+ * Parses the specific double option and fills in the value.
+ * Returns true if the option is found.
+ * Returns false if the option is not found or if there is an error parsing the
+ * value. If there is an error, the specified exec_state is also
+ * updated.
+ */
+bool LDBCommand::ParseDoubleOption(
+ const std::map<std::string, std::string>& /*options*/,
+ const std::string& option, double& value,
+ LDBCommandExecuteResult& exec_state) {
+ auto itr = option_map_.find(option);
+ if (itr != option_map_.end()) {
+#if defined(CYGWIN)
+ char* str_end = nullptr;
+ value = std::strtod(itr->second.c_str(), &str_end);
+ if (str_end == itr->second.c_str()) {
+ exec_state =
+ LDBCommandExecuteResult::Failed(option + " has an invalid value.");
+ } else if (errno == ERANGE) {
+ exec_state = LDBCommandExecuteResult::Failed(
+ option + " has a value out-of-range.");
+ } else {
+ return true;
+ }
+#else
+ try {
+ value = std::stod(itr->second);
+ return true;
+ } catch (const std::invalid_argument&) {
+ exec_state =
+ LDBCommandExecuteResult::Failed(option + " has an invalid value.");
+ } catch (const std::out_of_range&) {
+ exec_state = LDBCommandExecuteResult::Failed(
+ option + " has a value out-of-range.");
+ }
+#endif
+ }
+ return false;
+}
+
+/**
+ * Parses the specific integer option and fills in the value.
+ * Returns true if the option is found.
+ * Returns false if the option is not found or if there is an error parsing the
+ * value. If there is an error, the specified exec_state is also
+ * updated.
+ */
+bool LDBCommand::ParseIntOption(
+ const std::map<std::string, std::string>& /*options*/,
+ const std::string& option, int& value,
+ LDBCommandExecuteResult& exec_state) {
+ auto itr = option_map_.find(option);
+ if (itr != option_map_.end()) {
+#if defined(CYGWIN)
+ char* str_end = nullptr;
+ value = strtol(itr->second.c_str(), &str_end, 10);
+ if (str_end == itr->second.c_str()) {
+ exec_state =
+ LDBCommandExecuteResult::Failed(option + " has an invalid value.");
+ } else if (errno == ERANGE) {
+ exec_state = LDBCommandExecuteResult::Failed(
+ option + " has a value out-of-range.");
+ } else {
+ return true;
+ }
+#else
+ try {
+ value = std::stoi(itr->second);
+ return true;
+ } catch (const std::invalid_argument&) {
+ exec_state =
+ LDBCommandExecuteResult::Failed(option + " has an invalid value.");
+ } catch (const std::out_of_range&) {
+ exec_state = LDBCommandExecuteResult::Failed(
+ option + " has a value out-of-range.");
+ }
+#endif
+ }
+ return false;
+}
+
+/**
+ * Parses the specified option and fills in the value.
+ * Returns true if the option is found.
+ * Returns false otherwise.
+ */
+bool LDBCommand::ParseStringOption(
+ const std::map<std::string, std::string>& /*options*/,
+ const std::string& option, std::string* value) {
+ auto itr = option_map_.find(option);
+ if (itr != option_map_.end()) {
+ *value = itr->second;
+ return true;
+ }
+ return false;
+}
+
+/**
+ * Parses the specified compression type and fills in the value.
+ * Returns true if the compression type is found.
+ * Returns false otherwise.
+ */
+bool LDBCommand::ParseCompressionTypeOption(
+ const std::map<std::string, std::string>& /*options*/,
+ const std::string& option, CompressionType& value,
+ LDBCommandExecuteResult& exec_state) {
+ auto itr = option_map_.find(option);
+ if (itr != option_map_.end()) {
+ const std::string& comp = itr->second;
+ if (comp == "no") {
+ value = kNoCompression;
+ return true;
+ } else if (comp == "snappy") {
+ value = kSnappyCompression;
+ return true;
+ } else if (comp == "zlib") {
+ value = kZlibCompression;
+ return true;
+ } else if (comp == "bzip2") {
+ value = kBZip2Compression;
+ return true;
+ } else if (comp == "lz4") {
+ value = kLZ4Compression;
+ return true;
+ } else if (comp == "lz4hc") {
+ value = kLZ4HCCompression;
+ return true;
+ } else if (comp == "xpress") {
+ value = kXpressCompression;
+ return true;
+ } else if (comp == "zstd") {
+ value = kZSTD;
+ return true;
+ } else {
+ // Unknown compression.
+ exec_state = LDBCommandExecuteResult::Failed(
+ "Unknown compression algorithm: " + comp);
+ }
+ }
+ return false;
+}
+
+void LDBCommand::OverrideBaseOptions() {
+ options_.create_if_missing = false;
+
+ int db_write_buffer_size;
+ if (ParseIntOption(option_map_, ARG_DB_WRITE_BUFFER_SIZE,
+ db_write_buffer_size, exec_state_)) {
+ if (db_write_buffer_size >= 0) {
+ options_.db_write_buffer_size = db_write_buffer_size;
+ } else {
+ exec_state_ = LDBCommandExecuteResult::Failed(ARG_DB_WRITE_BUFFER_SIZE +
+ " must be >= 0.");
+ }
+ }
+
+ if (options_.db_paths.size() == 0) {
+ options_.db_paths.emplace_back(db_path_,
+ std::numeric_limits<uint64_t>::max());
+ }
+
+ OverrideBaseCFOptions(static_cast<ColumnFamilyOptions*>(&options_));
+}
+
+void LDBCommand::OverrideBaseCFOptions(ColumnFamilyOptions* cf_opts) {
+ BlockBasedTableOptions table_options;
+ bool use_table_options = false;
+ int bits;
+ if (ParseIntOption(option_map_, ARG_BLOOM_BITS, bits, exec_state_)) {
+ if (bits > 0) {
+ use_table_options = true;
+ table_options.filter_policy.reset(NewBloomFilterPolicy(bits));
+ } else {
+ exec_state_ =
+ LDBCommandExecuteResult::Failed(ARG_BLOOM_BITS + " must be > 0.");
+ }
+ }
+
+ int block_size;
+ if (ParseIntOption(option_map_, ARG_BLOCK_SIZE, block_size, exec_state_)) {
+ if (block_size > 0) {
+ use_table_options = true;
+ table_options.block_size = block_size;
+ } else {
+ exec_state_ =
+ LDBCommandExecuteResult::Failed(ARG_BLOCK_SIZE + " must be > 0.");
+ }
+ }
+
+ cf_opts->force_consistency_checks = force_consistency_checks_;
+ if (use_table_options) {
+ cf_opts->table_factory.reset(NewBlockBasedTableFactory(table_options));
+ }
+
+ cf_opts->enable_blob_files = enable_blob_files_;
+
+ int min_blob_size;
+ if (ParseIntOption(option_map_, ARG_MIN_BLOB_SIZE, min_blob_size,
+ exec_state_)) {
+ if (min_blob_size >= 0) {
+ cf_opts->min_blob_size = min_blob_size;
+ } else {
+ exec_state_ =
+ LDBCommandExecuteResult::Failed(ARG_MIN_BLOB_SIZE + " must be >= 0.");
+ }
+ }
+
+ int blob_file_size;
+ if (ParseIntOption(option_map_, ARG_BLOB_FILE_SIZE, blob_file_size,
+ exec_state_)) {
+ if (blob_file_size > 0) {
+ cf_opts->blob_file_size = blob_file_size;
+ } else {
+ exec_state_ =
+ LDBCommandExecuteResult::Failed(ARG_BLOB_FILE_SIZE + " must be > 0.");
+ }
+ }
+
+ cf_opts->enable_blob_garbage_collection = enable_blob_garbage_collection_;
+
+ double blob_garbage_collection_age_cutoff;
+ if (ParseDoubleOption(option_map_, ARG_BLOB_GARBAGE_COLLECTION_AGE_CUTOFF,
+ blob_garbage_collection_age_cutoff, exec_state_)) {
+ if (blob_garbage_collection_age_cutoff >= 0 &&
+ blob_garbage_collection_age_cutoff <= 1) {
+ cf_opts->blob_garbage_collection_age_cutoff =
+ blob_garbage_collection_age_cutoff;
+ } else {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ ARG_BLOB_GARBAGE_COLLECTION_AGE_CUTOFF + " must be >= 0 and <= 1.");
+ }
+ }
+
+ double blob_garbage_collection_force_threshold;
+ if (ParseDoubleOption(option_map_,
+ ARG_BLOB_GARBAGE_COLLECTION_FORCE_THRESHOLD,
+ blob_garbage_collection_force_threshold, exec_state_)) {
+ if (blob_garbage_collection_force_threshold >= 0 &&
+ blob_garbage_collection_force_threshold <= 1) {
+ cf_opts->blob_garbage_collection_force_threshold =
+ blob_garbage_collection_force_threshold;
+ } else {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ ARG_BLOB_GARBAGE_COLLECTION_FORCE_THRESHOLD +
+ " must be >= 0 and <= 1.");
+ }
+ }
+
+ int blob_compaction_readahead_size;
+ if (ParseIntOption(option_map_, ARG_BLOB_COMPACTION_READAHEAD_SIZE,
+ blob_compaction_readahead_size, exec_state_)) {
+ if (blob_compaction_readahead_size > 0) {
+ cf_opts->blob_compaction_readahead_size = blob_compaction_readahead_size;
+ } else {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ ARG_BLOB_COMPACTION_READAHEAD_SIZE + " must be > 0.");
+ }
+ }
+
+ int blob_file_starting_level;
+ if (ParseIntOption(option_map_, ARG_BLOB_FILE_STARTING_LEVEL,
+ blob_file_starting_level, exec_state_)) {
+ if (blob_file_starting_level >= 0) {
+ cf_opts->blob_file_starting_level = blob_file_starting_level;
+ } else {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ ARG_BLOB_FILE_STARTING_LEVEL + " must be >= 0.");
+ }
+ }
+
+ int prepopulate_blob_cache;
+ if (ParseIntOption(option_map_, ARG_PREPOPULATE_BLOB_CACHE,
+ prepopulate_blob_cache, exec_state_)) {
+ switch (prepopulate_blob_cache) {
+ case 0:
+ cf_opts->prepopulate_blob_cache = PrepopulateBlobCache::kDisable;
+ break;
+ case 1:
+ cf_opts->prepopulate_blob_cache = PrepopulateBlobCache::kFlushOnly;
+ break;
+ default:
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ ARG_PREPOPULATE_BLOB_CACHE +
+ " must be 0 (disable) or 1 (flush only).");
+ }
+ }
+
+ auto itr = option_map_.find(ARG_AUTO_COMPACTION);
+ if (itr != option_map_.end()) {
+ cf_opts->disable_auto_compactions = !StringToBool(itr->second);
+ }
+
+ CompressionType compression_type;
+ if (ParseCompressionTypeOption(option_map_, ARG_COMPRESSION_TYPE,
+ compression_type, exec_state_)) {
+ cf_opts->compression = compression_type;
+ }
+
+ CompressionType blob_compression_type;
+ if (ParseCompressionTypeOption(option_map_, ARG_BLOB_COMPRESSION_TYPE,
+ blob_compression_type, exec_state_)) {
+ cf_opts->blob_compression_type = blob_compression_type;
+ }
+
+ int compression_max_dict_bytes;
+ if (ParseIntOption(option_map_, ARG_COMPRESSION_MAX_DICT_BYTES,
+ compression_max_dict_bytes, exec_state_)) {
+ if (compression_max_dict_bytes >= 0) {
+ cf_opts->compression_opts.max_dict_bytes = compression_max_dict_bytes;
+ } else {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ ARG_COMPRESSION_MAX_DICT_BYTES + " must be >= 0.");
+ }
+ }
+
+ int write_buffer_size;
+ if (ParseIntOption(option_map_, ARG_WRITE_BUFFER_SIZE, write_buffer_size,
+ exec_state_)) {
+ if (write_buffer_size > 0) {
+ cf_opts->write_buffer_size = write_buffer_size;
+ } else {
+ exec_state_ = LDBCommandExecuteResult::Failed(ARG_WRITE_BUFFER_SIZE +
+ " must be > 0.");
+ }
+ }
+
+ int file_size;
+ if (ParseIntOption(option_map_, ARG_FILE_SIZE, file_size, exec_state_)) {
+ if (file_size > 0) {
+ cf_opts->target_file_size_base = file_size;
+ } else {
+ exec_state_ =
+ LDBCommandExecuteResult::Failed(ARG_FILE_SIZE + " must be > 0.");
+ }
+ }
+
+ int fix_prefix_len;
+ if (ParseIntOption(option_map_, ARG_FIX_PREFIX_LEN, fix_prefix_len,
+ exec_state_)) {
+ if (fix_prefix_len > 0) {
+ cf_opts->prefix_extractor.reset(
+ NewFixedPrefixTransform(static_cast<size_t>(fix_prefix_len)));
+ } else {
+ exec_state_ =
+ LDBCommandExecuteResult::Failed(ARG_FIX_PREFIX_LEN + " must be > 0.");
+ }
+ }
+}
+
+// First, initializes the options state using the OPTIONS file when enabled.
+// Second, overrides the options according to the CLI arguments and the
+// specific subcommand being run.
+void LDBCommand::PrepareOptions() {
+ if (!create_if_missing_ && try_load_options_) {
+ config_options_.env = options_.env;
+ Status s = LoadLatestOptions(config_options_, db_path_, &options_,
+ &column_families_);
+ if (!s.ok() && !s.IsNotFound()) {
+ // Option file exists but load option file error.
+ std::string msg = s.ToString();
+ exec_state_ = LDBCommandExecuteResult::Failed(msg);
+ db_ = nullptr;
+ return;
+ }
+ if (!options_.wal_dir.empty()) {
+ if (options_.env->FileExists(options_.wal_dir).IsNotFound()) {
+ options_.wal_dir = db_path_;
+ fprintf(
+ stderr,
+ "wal_dir loaded from the option file doesn't exist. Ignore it.\n");
+ }
+ }
+
+ // If merge operator is not set, set a string append operator.
+ for (auto& cf_entry : column_families_) {
+ if (!cf_entry.options.merge_operator) {
+ cf_entry.options.merge_operator =
+ MergeOperators::CreateStringAppendOperator(':');
+ }
+ }
+ }
+
+ if (options_.env == Env::Default()) {
+ options_.env = config_options_.env;
+ }
+
+ OverrideBaseOptions();
+ if (exec_state_.IsFailed()) {
+ return;
+ }
+
+ if (column_families_.empty()) {
+ // Reads the MANIFEST to figure out what column families exist. In this
+ // case, the option overrides from the CLI argument/specific subcommand
+ // apply to all column families.
+ std::vector<std::string> cf_list;
+ Status st = DB::ListColumnFamilies(options_, db_path_, &cf_list);
+ // It is possible the DB doesn't exist yet, for "create if not
+ // existing" case. The failure is ignored here. We rely on DB::Open()
+ // to give us the correct error message for problem with opening
+ // existing DB.
+ if (st.ok() && cf_list.size() > 1) {
+ // Ignore single column family DB.
+ for (auto cf_name : cf_list) {
+ column_families_.emplace_back(cf_name, options_);
+ }
+ }
+ } else {
+ // We got column families from the OPTIONS file. In this case, the option
+ // overrides from the CLI argument/specific subcommand only apply to the
+ // column family specified by `--column_family_name`.
+ auto column_families_iter =
+ std::find_if(column_families_.begin(), column_families_.end(),
+ [this](const ColumnFamilyDescriptor& cf_desc) {
+ return cf_desc.name == column_family_name_;
+ });
+ if (column_families_iter == column_families_.end()) {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ "Non-existing column family " + column_family_name_);
+ return;
+ }
+ OverrideBaseCFOptions(&column_families_iter->options);
+ }
+}
+
+bool LDBCommand::ParseKeyValue(const std::string& line, std::string* key,
+ std::string* value, bool is_key_hex,
+ bool is_value_hex) {
+ size_t pos = line.find(DELIM);
+ if (pos != std::string::npos) {
+ *key = line.substr(0, pos);
+ *value = line.substr(pos + strlen(DELIM));
+ if (is_key_hex) {
+ *key = HexToString(*key);
+ }
+ if (is_value_hex) {
+ *value = HexToString(*value);
+ }
+ return true;
+ } else {
+ return false;
+ }
+}
+
+/**
+ * Make sure that ONLY the command-line options and flags expected by this
+ * command are specified on the command-line. Extraneous options are usually
+ * the result of user error.
+ * Returns true if all checks pass. Else returns false, and prints an
+ * appropriate error msg to stderr.
+ */
+bool LDBCommand::ValidateCmdLineOptions() {
+ for (auto itr = option_map_.begin(); itr != option_map_.end(); ++itr) {
+ if (std::find(valid_cmd_line_options_.begin(),
+ valid_cmd_line_options_.end(),
+ itr->first) == valid_cmd_line_options_.end()) {
+ fprintf(stderr, "Invalid command-line option %s\n", itr->first.c_str());
+ return false;
+ }
+ }
+
+ for (std::vector<std::string>::const_iterator itr = flags_.begin();
+ itr != flags_.end(); ++itr) {
+ if (std::find(valid_cmd_line_options_.begin(),
+ valid_cmd_line_options_.end(),
+ *itr) == valid_cmd_line_options_.end()) {
+ fprintf(stderr, "Invalid command-line flag %s\n", itr->c_str());
+ return false;
+ }
+ }
+
+ if (!NoDBOpen() && option_map_.find(ARG_DB) == option_map_.end() &&
+ option_map_.find(ARG_PATH) == option_map_.end()) {
+ fprintf(stderr, "Either %s or %s must be specified.\n", ARG_DB.c_str(),
+ ARG_PATH.c_str());
+ return false;
+ }
+
+ return true;
+}
+
+std::string LDBCommand::HexToString(const std::string& str) {
+ std::string result;
+ std::string::size_type len = str.length();
+ if (len < 2 || str[0] != '0' || str[1] != 'x') {
+ fprintf(stderr, "Invalid hex input %s. Must start with 0x\n", str.c_str());
+ throw "Invalid hex input";
+ }
+ if (!Slice(str.data() + 2, len - 2).DecodeHex(&result)) {
+ throw "Invalid hex input";
+ }
+ return result;
+}
+
+std::string LDBCommand::StringToHex(const std::string& str) {
+ std::string result("0x");
+ result.append(Slice(str).ToString(true));
+ return result;
+}
+
+std::string LDBCommand::PrintKeyValue(const std::string& key,
+ const std::string& value, bool is_key_hex,
+ bool is_value_hex) {
+ std::string result;
+ result.append(is_key_hex ? StringToHex(key) : key);
+ result.append(DELIM);
+ result.append(is_value_hex ? StringToHex(value) : value);
+ return result;
+}
+
+std::string LDBCommand::PrintKeyValue(const std::string& key,
+ const std::string& value, bool is_hex) {
+ return PrintKeyValue(key, value, is_hex, is_hex);
+}
+
+std::string LDBCommand::HelpRangeCmdArgs() {
+ std::ostringstream str_stream;
+ str_stream << " ";
+ str_stream << "[--" << ARG_FROM << "] ";
+ str_stream << "[--" << ARG_TO << "] ";
+ return str_stream.str();
+}
+
+bool LDBCommand::IsKeyHex(const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags) {
+ return (IsFlagPresent(flags, ARG_HEX) || IsFlagPresent(flags, ARG_KEY_HEX) ||
+ ParseBooleanOption(options, ARG_HEX, false) ||
+ ParseBooleanOption(options, ARG_KEY_HEX, false));
+}
+
+bool LDBCommand::IsValueHex(const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags) {
+ return (IsFlagPresent(flags, ARG_HEX) ||
+ IsFlagPresent(flags, ARG_VALUE_HEX) ||
+ ParseBooleanOption(options, ARG_HEX, false) ||
+ ParseBooleanOption(options, ARG_VALUE_HEX, false));
+}
+
+bool LDBCommand::IsTryLoadOptions(
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags) {
+ if (IsFlagPresent(flags, ARG_TRY_LOAD_OPTIONS)) {
+ return true;
+ }
+ // if `DB` is specified and not explicitly to create a new db, default
+ // `try_load_options` to true. The user could still disable that by set
+ // `try_load_options=false`.
+ // Note: Opening as TTL DB doesn't support `try_load_options`, so it's default
+ // to false. TODO: TTL_DB may need to fix that, otherwise it's unable to open
+ // DB which has incompatible setting with default options.
+ bool default_val = (options.find(ARG_DB) != options.end()) &&
+ !IsFlagPresent(flags, ARG_CREATE_IF_MISSING) &&
+ !IsFlagPresent(flags, ARG_TTL);
+ return ParseBooleanOption(options, ARG_TRY_LOAD_OPTIONS, default_val);
+}
+
+bool LDBCommand::ParseBooleanOption(
+ const std::map<std::string, std::string>& options,
+ const std::string& option, bool default_val) {
+ auto itr = options.find(option);
+ if (itr != options.end()) {
+ std::string option_val = itr->second;
+ return StringToBool(itr->second);
+ }
+ return default_val;
+}
+
+bool LDBCommand::StringToBool(std::string val) {
+ std::transform(val.begin(), val.end(), val.begin(),
+ [](char ch) -> char { return (char)::tolower(ch); });
+
+ if (val == "true") {
+ return true;
+ } else if (val == "false") {
+ return false;
+ } else {
+ throw "Invalid value for boolean argument";
+ }
+}
+
+CompactorCommand::CompactorCommand(
+ const std::vector<std::string>& /*params*/,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags)
+ : LDBCommand(options, flags, false,
+ BuildCmdLineOptions({ARG_FROM, ARG_TO, ARG_HEX, ARG_KEY_HEX,
+ ARG_VALUE_HEX, ARG_TTL})),
+ null_from_(true),
+ null_to_(true) {
+ auto itr = options.find(ARG_FROM);
+ if (itr != options.end()) {
+ null_from_ = false;
+ from_ = itr->second;
+ }
+
+ itr = options.find(ARG_TO);
+ if (itr != options.end()) {
+ null_to_ = false;
+ to_ = itr->second;
+ }
+
+ if (is_key_hex_) {
+ if (!null_from_) {
+ from_ = HexToString(from_);
+ }
+ if (!null_to_) {
+ to_ = HexToString(to_);
+ }
+ }
+}
+
+void CompactorCommand::Help(std::string& ret) {
+ ret.append(" ");
+ ret.append(CompactorCommand::Name());
+ ret.append(HelpRangeCmdArgs());
+ ret.append("\n");
+}
+
+void CompactorCommand::DoCommand() {
+ if (!db_) {
+ assert(GetExecuteState().IsFailed());
+ return;
+ }
+
+ Slice* begin = nullptr;
+ Slice* end = nullptr;
+ if (!null_from_) {
+ begin = new Slice(from_);
+ }
+ if (!null_to_) {
+ end = new Slice(to_);
+ }
+
+ CompactRangeOptions cro;
+ cro.bottommost_level_compaction = BottommostLevelCompaction::kForceOptimized;
+
+ Status s = db_->CompactRange(cro, GetCfHandle(), begin, end);
+ if (!s.ok()) {
+ std::stringstream oss;
+ oss << "Compaction failed: " << s.ToString();
+ exec_state_ = LDBCommandExecuteResult::Failed(oss.str());
+ } else {
+ exec_state_ = LDBCommandExecuteResult::Succeed("");
+ }
+
+ delete begin;
+ delete end;
+}
+
+// ---------------------------------------------------------------------------
+const std::string DBLoaderCommand::ARG_DISABLE_WAL = "disable_wal";
+const std::string DBLoaderCommand::ARG_BULK_LOAD = "bulk_load";
+const std::string DBLoaderCommand::ARG_COMPACT = "compact";
+
+DBLoaderCommand::DBLoaderCommand(
+ const std::vector<std::string>& /*params*/,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags)
+ : LDBCommand(
+ options, flags, false,
+ BuildCmdLineOptions({ARG_HEX, ARG_KEY_HEX, ARG_VALUE_HEX, ARG_FROM,
+ ARG_TO, ARG_CREATE_IF_MISSING, ARG_DISABLE_WAL,
+ ARG_BULK_LOAD, ARG_COMPACT})),
+ disable_wal_(false),
+ bulk_load_(false),
+ compact_(false) {
+ create_if_missing_ = IsFlagPresent(flags, ARG_CREATE_IF_MISSING);
+ disable_wal_ = IsFlagPresent(flags, ARG_DISABLE_WAL);
+ bulk_load_ = IsFlagPresent(flags, ARG_BULK_LOAD);
+ compact_ = IsFlagPresent(flags, ARG_COMPACT);
+}
+
+void DBLoaderCommand::Help(std::string& ret) {
+ ret.append(" ");
+ ret.append(DBLoaderCommand::Name());
+ ret.append(" [--" + ARG_CREATE_IF_MISSING + "]");
+ ret.append(" [--" + ARG_DISABLE_WAL + "]");
+ ret.append(" [--" + ARG_BULK_LOAD + "]");
+ ret.append(" [--" + ARG_COMPACT + "]");
+ ret.append("\n");
+}
+
+void DBLoaderCommand::OverrideBaseOptions() {
+ LDBCommand::OverrideBaseOptions();
+ options_.create_if_missing = create_if_missing_;
+ if (bulk_load_) {
+ options_.PrepareForBulkLoad();
+ }
+}
+
+void DBLoaderCommand::DoCommand() {
+ if (!db_) {
+ assert(GetExecuteState().IsFailed());
+ return;
+ }
+
+ WriteOptions write_options;
+ if (disable_wal_) {
+ write_options.disableWAL = true;
+ }
+
+ int bad_lines = 0;
+ std::string line;
+ // prefer ifstream getline performance vs that from std::cin istream
+ std::ifstream ifs_stdin("/dev/stdin");
+ std::istream* istream_p = ifs_stdin.is_open() ? &ifs_stdin : &std::cin;
+ Status s;
+ while (s.ok() && getline(*istream_p, line, '\n')) {
+ std::string key;
+ std::string value;
+ if (ParseKeyValue(line, &key, &value, is_key_hex_, is_value_hex_)) {
+ s = db_->Put(write_options, GetCfHandle(), Slice(key), Slice(value));
+ } else if (0 == line.find("Keys in range:")) {
+ // ignore this line
+ } else if (0 == line.find("Created bg thread 0x")) {
+ // ignore this line
+ } else {
+ bad_lines++;
+ }
+ }
+
+ if (bad_lines > 0) {
+ std::cout << "Warning: " << bad_lines << " bad lines ignored." << std::endl;
+ }
+ if (!s.ok()) {
+ std::stringstream oss;
+ oss << "Load failed: " << s.ToString();
+ exec_state_ = LDBCommandExecuteResult::Failed(oss.str());
+ }
+ if (compact_ && s.ok()) {
+ s = db_->CompactRange(CompactRangeOptions(), GetCfHandle(), nullptr,
+ nullptr);
+ }
+ if (!s.ok()) {
+ std::stringstream oss;
+ oss << "Compaction failed: " << s.ToString();
+ exec_state_ = LDBCommandExecuteResult::Failed(oss.str());
+ }
+}
+
+// ----------------------------------------------------------------------------
+
+namespace {
+
+void DumpManifestFile(Options options, std::string file, bool verbose, bool hex,
+ bool json) {
+ EnvOptions sopt;
+ std::string dbname("dummy");
+ std::shared_ptr<Cache> tc(NewLRUCache(options.max_open_files - 10,
+ options.table_cache_numshardbits));
+ // Notice we are using the default options not through SanitizeOptions(),
+ // if VersionSet::DumpManifest() depends on any option done by
+ // SanitizeOptions(), we need to initialize it manually.
+ options.db_paths.emplace_back("dummy", 0);
+ options.num_levels = 64;
+ WriteController wc(options.delayed_write_rate);
+ WriteBufferManager wb(options.db_write_buffer_size);
+ ImmutableDBOptions immutable_db_options(options);
+ VersionSet versions(dbname, &immutable_db_options, sopt, tc.get(), &wb, &wc,
+ /*block_cache_tracer=*/nullptr, /*io_tracer=*/nullptr,
+ /*db_id*/ "", /*db_session_id*/ "");
+ Status s = versions.DumpManifest(options, file, verbose, hex, json);
+ if (!s.ok()) {
+ fprintf(stderr, "Error in processing file %s %s\n", file.c_str(),
+ s.ToString().c_str());
+ }
+}
+
+} // namespace
+
+const std::string ManifestDumpCommand::ARG_VERBOSE = "verbose";
+const std::string ManifestDumpCommand::ARG_JSON = "json";
+const std::string ManifestDumpCommand::ARG_PATH = "path";
+
+void ManifestDumpCommand::Help(std::string& ret) {
+ ret.append(" ");
+ ret.append(ManifestDumpCommand::Name());
+ ret.append(" [--" + ARG_VERBOSE + "]");
+ ret.append(" [--" + ARG_JSON + "]");
+ ret.append(" [--" + ARG_PATH + "=<path_to_manifest_file>]");
+ ret.append("\n");
+}
+
+ManifestDumpCommand::ManifestDumpCommand(
+ const std::vector<std::string>& /*params*/,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags)
+ : LDBCommand(
+ options, flags, false,
+ BuildCmdLineOptions({ARG_VERBOSE, ARG_PATH, ARG_HEX, ARG_JSON})),
+ verbose_(false),
+ json_(false),
+ path_("") {
+ verbose_ = IsFlagPresent(flags, ARG_VERBOSE);
+ json_ = IsFlagPresent(flags, ARG_JSON);
+
+ auto itr = options.find(ARG_PATH);
+ if (itr != options.end()) {
+ path_ = itr->second;
+ if (path_.empty()) {
+ exec_state_ = LDBCommandExecuteResult::Failed("--path: missing pathname");
+ }
+ }
+}
+
+void ManifestDumpCommand::DoCommand() {
+ std::string manifestfile;
+
+ if (!path_.empty()) {
+ manifestfile = path_;
+ } else {
+ // We need to find the manifest file by searching the directory
+ // containing the db for files of the form MANIFEST_[0-9]+
+
+ std::vector<std::string> files;
+ Status s = options_.env->GetChildren(db_path_, &files);
+ if (!s.ok()) {
+ std::string err_msg = s.ToString();
+ err_msg.append(": Failed to list the content of ");
+ err_msg.append(db_path_);
+ exec_state_ = LDBCommandExecuteResult::Failed(err_msg);
+ return;
+ }
+ const std::string kManifestNamePrefix = "MANIFEST-";
+ std::string matched_file;
+#ifdef OS_WIN
+ const char kPathDelim = '\\';
+#else
+ const char kPathDelim = '/';
+#endif
+ for (const auto& file_path : files) {
+ // Some Env::GetChildren() return absolute paths. Some directories' path
+ // end with path delim, e.g. '/' or '\\'.
+ size_t pos = file_path.find_last_of(kPathDelim);
+ if (pos == file_path.size() - 1) {
+ continue;
+ }
+ std::string fname;
+ if (pos != std::string::npos) {
+ // Absolute path.
+ fname.assign(file_path, pos + 1, file_path.size() - pos - 1);
+ } else {
+ fname = file_path;
+ }
+ uint64_t file_num = 0;
+ FileType file_type = kWalFile; // Just for initialization
+ if (ParseFileName(fname, &file_num, &file_type) &&
+ file_type == kDescriptorFile) {
+ if (!matched_file.empty()) {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ "Multiple MANIFEST files found; use --path to select one");
+ return;
+ } else {
+ matched_file.swap(fname);
+ }
+ }
+ }
+ if (matched_file.empty()) {
+ std::string err_msg("No MANIFEST found in ");
+ err_msg.append(db_path_);
+ exec_state_ = LDBCommandExecuteResult::Failed(err_msg);
+ return;
+ }
+ if (db_path_.back() != '/') {
+ db_path_.append("/");
+ }
+ manifestfile = db_path_ + matched_file;
+ }
+
+ if (verbose_) {
+ fprintf(stdout, "Processing Manifest file %s\n", manifestfile.c_str());
+ }
+
+ DumpManifestFile(options_, manifestfile, verbose_, is_key_hex_, json_);
+
+ if (verbose_) {
+ fprintf(stdout, "Processing Manifest file %s done\n", manifestfile.c_str());
+ }
+}
+
+// ----------------------------------------------------------------------------
+namespace {
+
+Status GetLiveFilesChecksumInfoFromVersionSet(Options options,
+ const std::string& db_path,
+ FileChecksumList* checksum_list) {
+ EnvOptions sopt;
+ Status s;
+ std::string dbname(db_path);
+ std::shared_ptr<Cache> tc(NewLRUCache(options.max_open_files - 10,
+ options.table_cache_numshardbits));
+ // Notice we are using the default options not through SanitizeOptions(),
+ // if VersionSet::GetLiveFilesChecksumInfo depends on any option done by
+ // SanitizeOptions(), we need to initialize it manually.
+ options.db_paths.emplace_back(db_path, 0);
+ options.num_levels = 64;
+ WriteController wc(options.delayed_write_rate);
+ WriteBufferManager wb(options.db_write_buffer_size);
+ ImmutableDBOptions immutable_db_options(options);
+ VersionSet versions(dbname, &immutable_db_options, sopt, tc.get(), &wb, &wc,
+ /*block_cache_tracer=*/nullptr, /*io_tracer=*/nullptr,
+ /*db_id*/ "", /*db_session_id*/ "");
+ std::vector<std::string> cf_name_list;
+ s = versions.ListColumnFamilies(&cf_name_list, db_path,
+ immutable_db_options.fs.get());
+ if (s.ok()) {
+ std::vector<ColumnFamilyDescriptor> cf_list;
+ for (const auto& name : cf_name_list) {
+ cf_list.emplace_back(name, ColumnFamilyOptions(options));
+ }
+ s = versions.Recover(cf_list, true);
+ }
+ if (s.ok()) {
+ s = versions.GetLiveFilesChecksumInfo(checksum_list);
+ }
+ return s;
+}
+
+} // namespace
+
+const std::string FileChecksumDumpCommand::ARG_PATH = "path";
+
+void FileChecksumDumpCommand::Help(std::string& ret) {
+ ret.append(" ");
+ ret.append(FileChecksumDumpCommand::Name());
+ ret.append(" [--" + ARG_PATH + "=<path_to_manifest_file>]");
+ ret.append("\n");
+}
+
+FileChecksumDumpCommand::FileChecksumDumpCommand(
+ const std::vector<std::string>& /*params*/,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags)
+ : LDBCommand(options, flags, false,
+ BuildCmdLineOptions({ARG_PATH, ARG_HEX})),
+ path_("") {
+ auto itr = options.find(ARG_PATH);
+ if (itr != options.end()) {
+ path_ = itr->second;
+ if (path_.empty()) {
+ exec_state_ = LDBCommandExecuteResult::Failed("--path: missing pathname");
+ }
+ }
+ is_checksum_hex_ = IsFlagPresent(flags, ARG_HEX);
+}
+
+void FileChecksumDumpCommand::DoCommand() {
+ // print out the checksum information in the following format:
+ // sst file number, checksum function name, checksum value
+ // sst file number, checksum function name, checksum value
+ // ......
+
+ std::unique_ptr<FileChecksumList> checksum_list(NewFileChecksumList());
+ Status s = GetLiveFilesChecksumInfoFromVersionSet(options_, db_path_,
+ checksum_list.get());
+ if (s.ok() && checksum_list != nullptr) {
+ std::vector<uint64_t> file_numbers;
+ std::vector<std::string> checksums;
+ std::vector<std::string> checksum_func_names;
+ s = checksum_list->GetAllFileChecksums(&file_numbers, &checksums,
+ &checksum_func_names);
+ if (s.ok()) {
+ for (size_t i = 0; i < file_numbers.size(); i++) {
+ assert(i < file_numbers.size());
+ assert(i < checksums.size());
+ assert(i < checksum_func_names.size());
+ std::string checksum;
+ if (is_checksum_hex_) {
+ checksum = StringToHex(checksums[i]);
+ } else {
+ checksum = std::move(checksums[i]);
+ }
+ fprintf(stdout, "%" PRId64 ", %s, %s\n", file_numbers[i],
+ checksum_func_names[i].c_str(), checksum.c_str());
+ }
+ fprintf(stdout, "Print SST file checksum information finished \n");
+ }
+ }
+
+ if (!s.ok()) {
+ exec_state_ = LDBCommandExecuteResult::Failed(s.ToString());
+ }
+}
+
+// ----------------------------------------------------------------------------
+
+void GetPropertyCommand::Help(std::string& ret) {
+ ret.append(" ");
+ ret.append(GetPropertyCommand::Name());
+ ret.append(" <property_name>");
+ ret.append("\n");
+}
+
+GetPropertyCommand::GetPropertyCommand(
+ const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags)
+ : LDBCommand(options, flags, true, BuildCmdLineOptions({})) {
+ if (params.size() != 1) {
+ exec_state_ =
+ LDBCommandExecuteResult::Failed("property name must be specified");
+ } else {
+ property_ = params[0];
+ }
+}
+
+void GetPropertyCommand::DoCommand() {
+ if (!db_) {
+ assert(GetExecuteState().IsFailed());
+ return;
+ }
+
+ std::map<std::string, std::string> value_map;
+ std::string value;
+
+ // Rather than having different ldb command for map properties vs. string
+ // properties, we simply try Map property first. (This order only chosen
+ // because I prefer the map-style output for
+ // "rocksdb.aggregated-table-properties".)
+ if (db_->GetMapProperty(GetCfHandle(), property_, &value_map)) {
+ if (value_map.empty()) {
+ fprintf(stdout, "%s: <empty map>\n", property_.c_str());
+ } else {
+ for (auto& e : value_map) {
+ fprintf(stdout, "%s.%s: %s\n", property_.c_str(), e.first.c_str(),
+ e.second.c_str());
+ }
+ }
+ } else if (db_->GetProperty(GetCfHandle(), property_, &value)) {
+ fprintf(stdout, "%s: %s\n", property_.c_str(), value.c_str());
+ } else {
+ exec_state_ =
+ LDBCommandExecuteResult::Failed("failed to get property: " + property_);
+ }
+}
+
+// ----------------------------------------------------------------------------
+
+void ListColumnFamiliesCommand::Help(std::string& ret) {
+ ret.append(" ");
+ ret.append(ListColumnFamiliesCommand::Name());
+ ret.append("\n");
+}
+
+ListColumnFamiliesCommand::ListColumnFamiliesCommand(
+ const std::vector<std::string>& /*params*/,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags)
+ : LDBCommand(options, flags, false, BuildCmdLineOptions({})) {}
+
+void ListColumnFamiliesCommand::DoCommand() {
+ std::vector<std::string> column_families;
+ Status s = DB::ListColumnFamilies(options_, db_path_, &column_families);
+ if (!s.ok()) {
+ fprintf(stderr, "Error in processing db %s %s\n", db_path_.c_str(),
+ s.ToString().c_str());
+ } else {
+ fprintf(stdout, "Column families in %s: \n{", db_path_.c_str());
+ bool first = true;
+ for (auto cf : column_families) {
+ if (!first) {
+ fprintf(stdout, ", ");
+ }
+ first = false;
+ fprintf(stdout, "%s", cf.c_str());
+ }
+ fprintf(stdout, "}\n");
+ }
+}
+
+void CreateColumnFamilyCommand::Help(std::string& ret) {
+ ret.append(" ");
+ ret.append(CreateColumnFamilyCommand::Name());
+ ret.append(" --db=<db_path> <new_column_family_name>");
+ ret.append("\n");
+}
+
+CreateColumnFamilyCommand::CreateColumnFamilyCommand(
+ const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags)
+ : LDBCommand(options, flags, true, {ARG_DB}) {
+ if (params.size() != 1) {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ "new column family name must be specified");
+ } else {
+ new_cf_name_ = params[0];
+ }
+}
+
+void CreateColumnFamilyCommand::DoCommand() {
+ if (!db_) {
+ assert(GetExecuteState().IsFailed());
+ return;
+ }
+ ColumnFamilyHandle* new_cf_handle = nullptr;
+ Status st = db_->CreateColumnFamily(options_, new_cf_name_, &new_cf_handle);
+ if (st.ok()) {
+ fprintf(stdout, "OK\n");
+ } else {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ "Fail to create new column family: " + st.ToString());
+ }
+ delete new_cf_handle;
+ CloseDB();
+}
+
+void DropColumnFamilyCommand::Help(std::string& ret) {
+ ret.append(" ");
+ ret.append(DropColumnFamilyCommand::Name());
+ ret.append(" --db=<db_path> <column_family_name_to_drop>");
+ ret.append("\n");
+}
+
+DropColumnFamilyCommand::DropColumnFamilyCommand(
+ const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags)
+ : LDBCommand(options, flags, true, {ARG_DB}) {
+ if (params.size() != 1) {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ "The name of column family to drop must be specified");
+ } else {
+ cf_name_to_drop_ = params[0];
+ }
+}
+
+void DropColumnFamilyCommand::DoCommand() {
+ if (!db_) {
+ assert(GetExecuteState().IsFailed());
+ return;
+ }
+ auto iter = cf_handles_.find(cf_name_to_drop_);
+ if (iter == cf_handles_.end()) {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ "Column family: " + cf_name_to_drop_ + " doesn't exist in db.");
+ return;
+ }
+ ColumnFamilyHandle* cf_handle_to_drop = iter->second;
+ Status st = db_->DropColumnFamily(cf_handle_to_drop);
+ if (st.ok()) {
+ fprintf(stdout, "OK\n");
+ } else {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ "Fail to drop column family: " + st.ToString());
+ }
+ CloseDB();
+}
+
+// ----------------------------------------------------------------------------
+namespace {
+
+// This function only called when it's the sane case of >1 buckets in time-range
+// Also called only when timekv falls between ttl_start and ttl_end provided
+void IncBucketCounts(std::vector<uint64_t>& bucket_counts, int ttl_start,
+ int time_range, int bucket_size, int timekv,
+ int num_buckets) {
+#ifdef NDEBUG
+ (void)time_range;
+ (void)num_buckets;
+#endif
+ assert(time_range > 0 && timekv >= ttl_start && bucket_size > 0 &&
+ timekv < (ttl_start + time_range) && num_buckets > 1);
+ int bucket = (timekv - ttl_start) / bucket_size;
+ bucket_counts[bucket]++;
+}
+
+void PrintBucketCounts(const std::vector<uint64_t>& bucket_counts,
+ int ttl_start, int ttl_end, int bucket_size,
+ int num_buckets) {
+ int time_point = ttl_start;
+ for (int i = 0; i < num_buckets - 1; i++, time_point += bucket_size) {
+ fprintf(stdout, "Keys in range %s to %s : %lu\n",
+ TimeToHumanString(time_point).c_str(),
+ TimeToHumanString(time_point + bucket_size).c_str(),
+ (unsigned long)bucket_counts[i]);
+ }
+ fprintf(stdout, "Keys in range %s to %s : %lu\n",
+ TimeToHumanString(time_point).c_str(),
+ TimeToHumanString(ttl_end).c_str(),
+ (unsigned long)bucket_counts[num_buckets - 1]);
+}
+
+} // namespace
+
+const std::string InternalDumpCommand::ARG_COUNT_ONLY = "count_only";
+const std::string InternalDumpCommand::ARG_COUNT_DELIM = "count_delim";
+const std::string InternalDumpCommand::ARG_STATS = "stats";
+const std::string InternalDumpCommand::ARG_INPUT_KEY_HEX = "input_key_hex";
+
+InternalDumpCommand::InternalDumpCommand(
+ const std::vector<std::string>& /*params*/,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags)
+ : LDBCommand(options, flags, true,
+ BuildCmdLineOptions(
+ {ARG_HEX, ARG_KEY_HEX, ARG_VALUE_HEX, ARG_FROM, ARG_TO,
+ ARG_MAX_KEYS, ARG_COUNT_ONLY, ARG_COUNT_DELIM, ARG_STATS,
+ ARG_INPUT_KEY_HEX, ARG_DECODE_BLOB_INDEX})),
+ has_from_(false),
+ has_to_(false),
+ max_keys_(-1),
+ delim_("."),
+ count_only_(false),
+ count_delim_(false),
+ print_stats_(false),
+ is_input_key_hex_(false),
+ decode_blob_index_(false) {
+ has_from_ = ParseStringOption(options, ARG_FROM, &from_);
+ has_to_ = ParseStringOption(options, ARG_TO, &to_);
+
+ ParseIntOption(options, ARG_MAX_KEYS, max_keys_, exec_state_);
+ auto itr = options.find(ARG_COUNT_DELIM);
+ if (itr != options.end()) {
+ delim_ = itr->second;
+ count_delim_ = true;
+ // fprintf(stdout,"delim = %c\n",delim_[0]);
+ } else {
+ count_delim_ = IsFlagPresent(flags, ARG_COUNT_DELIM);
+ delim_ = ".";
+ }
+
+ print_stats_ = IsFlagPresent(flags, ARG_STATS);
+ count_only_ = IsFlagPresent(flags, ARG_COUNT_ONLY);
+ is_input_key_hex_ = IsFlagPresent(flags, ARG_INPUT_KEY_HEX);
+ decode_blob_index_ = IsFlagPresent(flags, ARG_DECODE_BLOB_INDEX);
+
+ if (is_input_key_hex_) {
+ if (has_from_) {
+ from_ = HexToString(from_);
+ }
+ if (has_to_) {
+ to_ = HexToString(to_);
+ }
+ }
+}
+
+void InternalDumpCommand::Help(std::string& ret) {
+ ret.append(" ");
+ ret.append(InternalDumpCommand::Name());
+ ret.append(HelpRangeCmdArgs());
+ ret.append(" [--" + ARG_INPUT_KEY_HEX + "]");
+ ret.append(" [--" + ARG_MAX_KEYS + "=<N>]");
+ ret.append(" [--" + ARG_COUNT_ONLY + "]");
+ ret.append(" [--" + ARG_COUNT_DELIM + "=<char>]");
+ ret.append(" [--" + ARG_STATS + "]");
+ ret.append(" [--" + ARG_DECODE_BLOB_INDEX + "]");
+ ret.append("\n");
+}
+
+void InternalDumpCommand::DoCommand() {
+ if (!db_) {
+ assert(GetExecuteState().IsFailed());
+ return;
+ }
+
+ if (print_stats_) {
+ std::string stats;
+ if (db_->GetProperty(GetCfHandle(), "rocksdb.stats", &stats)) {
+ fprintf(stdout, "%s\n", stats.c_str());
+ }
+ }
+
+ // Cast as DBImpl to get internal iterator
+ std::vector<KeyVersion> key_versions;
+ Status st = GetAllKeyVersions(db_, GetCfHandle(), from_, to_, max_keys_,
+ &key_versions);
+ if (!st.ok()) {
+ exec_state_ = LDBCommandExecuteResult::Failed(st.ToString());
+ return;
+ }
+ std::string rtype1, rtype2, row, val;
+ rtype2 = "";
+ uint64_t c = 0;
+ uint64_t s1 = 0, s2 = 0;
+
+ long long count = 0;
+ for (auto& key_version : key_versions) {
+ ValueType value_type = static_cast<ValueType>(key_version.type);
+ InternalKey ikey(key_version.user_key, key_version.sequence, value_type);
+ if (has_to_ && ikey.user_key() == to_) {
+ // GetAllKeyVersions() includes keys with user key `to_`, but idump has
+ // traditionally excluded such keys.
+ break;
+ }
+ ++count;
+ int k;
+ if (count_delim_) {
+ rtype1 = "";
+ s1 = 0;
+ row = ikey.Encode().ToString();
+ val = key_version.value;
+ for (k = 0; row[k] != '\x01' && row[k] != '\0'; k++) s1++;
+ for (k = 0; val[k] != '\x01' && val[k] != '\0'; k++) s1++;
+ for (int j = 0; row[j] != delim_[0] && row[j] != '\0' && row[j] != '\x01';
+ j++)
+ rtype1 += row[j];
+ if (rtype2.compare("") && rtype2.compare(rtype1) != 0) {
+ fprintf(stdout, "%s => count:%" PRIu64 "\tsize:%" PRIu64 "\n",
+ rtype2.c_str(), c, s2);
+ c = 1;
+ s2 = s1;
+ rtype2 = rtype1;
+ } else {
+ c++;
+ s2 += s1;
+ rtype2 = rtype1;
+ }
+ }
+
+ if (!count_only_ && !count_delim_) {
+ std::string key = ikey.DebugString(is_key_hex_);
+ Slice value(key_version.value);
+ if (!decode_blob_index_ || value_type != kTypeBlobIndex) {
+ fprintf(stdout, "%s => %s\n", key.c_str(),
+ value.ToString(is_value_hex_).c_str());
+ } else {
+ BlobIndex blob_index;
+
+ const Status s = blob_index.DecodeFrom(value);
+ if (!s.ok()) {
+ fprintf(stderr, "%s => error decoding blob index =>\n", key.c_str());
+ } else {
+ fprintf(stdout, "%s => %s\n", key.c_str(),
+ blob_index.DebugString(is_value_hex_).c_str());
+ }
+ }
+ }
+
+ // Terminate if maximum number of keys have been dumped
+ if (max_keys_ > 0 && count >= max_keys_) break;
+ }
+ if (count_delim_) {
+ fprintf(stdout, "%s => count:%" PRIu64 "\tsize:%" PRIu64 "\n",
+ rtype2.c_str(), c, s2);
+ } else {
+ fprintf(stdout, "Internal keys in range: %lld\n", count);
+ }
+}
+
+const std::string DBDumperCommand::ARG_COUNT_ONLY = "count_only";
+const std::string DBDumperCommand::ARG_COUNT_DELIM = "count_delim";
+const std::string DBDumperCommand::ARG_STATS = "stats";
+const std::string DBDumperCommand::ARG_TTL_BUCKET = "bucket";
+
+DBDumperCommand::DBDumperCommand(
+ const std::vector<std::string>& /*params*/,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags)
+ : LDBCommand(
+ options, flags, true,
+ BuildCmdLineOptions(
+ {ARG_TTL, ARG_HEX, ARG_KEY_HEX, ARG_VALUE_HEX, ARG_FROM, ARG_TO,
+ ARG_MAX_KEYS, ARG_COUNT_ONLY, ARG_COUNT_DELIM, ARG_STATS,
+ ARG_TTL_START, ARG_TTL_END, ARG_TTL_BUCKET, ARG_TIMESTAMP,
+ ARG_PATH, ARG_DECODE_BLOB_INDEX, ARG_DUMP_UNCOMPRESSED_BLOBS})),
+ null_from_(true),
+ null_to_(true),
+ max_keys_(-1),
+ count_only_(false),
+ count_delim_(false),
+ print_stats_(false),
+ decode_blob_index_(false) {
+ auto itr = options.find(ARG_FROM);
+ if (itr != options.end()) {
+ null_from_ = false;
+ from_ = itr->second;
+ }
+
+ itr = options.find(ARG_TO);
+ if (itr != options.end()) {
+ null_to_ = false;
+ to_ = itr->second;
+ }
+
+ itr = options.find(ARG_MAX_KEYS);
+ if (itr != options.end()) {
+ try {
+#if defined(CYGWIN)
+ max_keys_ = strtol(itr->second.c_str(), 0, 10);
+#else
+ max_keys_ = std::stoi(itr->second);
+#endif
+ } catch (const std::invalid_argument&) {
+ exec_state_ = LDBCommandExecuteResult::Failed(ARG_MAX_KEYS +
+ " has an invalid value");
+ } catch (const std::out_of_range&) {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ ARG_MAX_KEYS + " has a value out-of-range");
+ }
+ }
+ itr = options.find(ARG_COUNT_DELIM);
+ if (itr != options.end()) {
+ delim_ = itr->second;
+ count_delim_ = true;
+ } else {
+ count_delim_ = IsFlagPresent(flags, ARG_COUNT_DELIM);
+ delim_ = ".";
+ }
+
+ print_stats_ = IsFlagPresent(flags, ARG_STATS);
+ count_only_ = IsFlagPresent(flags, ARG_COUNT_ONLY);
+ decode_blob_index_ = IsFlagPresent(flags, ARG_DECODE_BLOB_INDEX);
+ dump_uncompressed_blobs_ = IsFlagPresent(flags, ARG_DUMP_UNCOMPRESSED_BLOBS);
+
+ if (is_key_hex_) {
+ if (!null_from_) {
+ from_ = HexToString(from_);
+ }
+ if (!null_to_) {
+ to_ = HexToString(to_);
+ }
+ }
+
+ itr = options.find(ARG_PATH);
+ if (itr != options.end()) {
+ path_ = itr->second;
+ if (db_path_.empty()) {
+ db_path_ = path_;
+ }
+ }
+}
+
+void DBDumperCommand::Help(std::string& ret) {
+ ret.append(" ");
+ ret.append(DBDumperCommand::Name());
+ ret.append(HelpRangeCmdArgs());
+ ret.append(" [--" + ARG_TTL + "]");
+ ret.append(" [--" + ARG_MAX_KEYS + "=<N>]");
+ ret.append(" [--" + ARG_TIMESTAMP + "]");
+ ret.append(" [--" + ARG_COUNT_ONLY + "]");
+ ret.append(" [--" + ARG_COUNT_DELIM + "=<char>]");
+ ret.append(" [--" + ARG_STATS + "]");
+ ret.append(" [--" + ARG_TTL_BUCKET + "=<N>]");
+ ret.append(" [--" + ARG_TTL_START + "=<N>:- is inclusive]");
+ ret.append(" [--" + ARG_TTL_END + "=<N>:- is exclusive]");
+ ret.append(" [--" + ARG_PATH + "=<path_to_a_file>]");
+ ret.append(" [--" + ARG_DECODE_BLOB_INDEX + "]");
+ ret.append(" [--" + ARG_DUMP_UNCOMPRESSED_BLOBS + "]");
+ ret.append("\n");
+}
+
+/**
+ * Handles two separate cases:
+ *
+ * 1) --db is specified - just dump the database.
+ *
+ * 2) --path is specified - determine based on file extension what dumping
+ * function to call. Please note that we intentionally use the extension
+ * and avoid probing the file contents under the assumption that renaming
+ * the files is not a supported scenario.
+ *
+ */
+void DBDumperCommand::DoCommand() {
+ if (!db_) {
+ assert(!path_.empty());
+ std::string fileName = GetFileNameFromPath(path_);
+ uint64_t number;
+ FileType type;
+
+ exec_state_ = LDBCommandExecuteResult::Succeed("");
+
+ if (!ParseFileName(fileName, &number, &type)) {
+ exec_state_ =
+ LDBCommandExecuteResult::Failed("Can't parse file type: " + path_);
+ return;
+ }
+
+ switch (type) {
+ case kWalFile:
+ // TODO(myabandeh): allow configuring is_write_commited
+ DumpWalFile(options_, path_, /* print_header_ */ true,
+ /* print_values_ */ true, true /* is_write_commited */,
+ &exec_state_);
+ break;
+ case kTableFile:
+ DumpSstFile(options_, path_, is_key_hex_, /* show_properties */ true,
+ decode_blob_index_, from_, to_);
+ break;
+ case kDescriptorFile:
+ DumpManifestFile(options_, path_, /* verbose_ */ false, is_key_hex_,
+ /* json_ */ false);
+ break;
+ case kBlobFile:
+ DumpBlobFile(path_, is_key_hex_, is_value_hex_,
+ dump_uncompressed_blobs_);
+ break;
+ default:
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ "File type not supported: " + path_);
+ break;
+ }
+
+ } else {
+ DoDumpCommand();
+ }
+}
+
+void DBDumperCommand::DoDumpCommand() {
+ assert(nullptr != db_);
+ assert(path_.empty());
+
+ // Parse command line args
+ uint64_t count = 0;
+ if (print_stats_) {
+ std::string stats;
+ if (db_->GetProperty("rocksdb.stats", &stats)) {
+ fprintf(stdout, "%s\n", stats.c_str());
+ }
+ }
+
+ // Setup key iterator
+ ReadOptions scan_read_opts;
+ scan_read_opts.total_order_seek = true;
+ Iterator* iter = db_->NewIterator(scan_read_opts, GetCfHandle());
+ Status st = iter->status();
+ if (!st.ok()) {
+ exec_state_ =
+ LDBCommandExecuteResult::Failed("Iterator error." + st.ToString());
+ }
+
+ if (!null_from_) {
+ iter->Seek(from_);
+ } else {
+ iter->SeekToFirst();
+ }
+
+ int max_keys = max_keys_;
+ int ttl_start;
+ if (!ParseIntOption(option_map_, ARG_TTL_START, ttl_start, exec_state_)) {
+ ttl_start = DBWithTTLImpl::kMinTimestamp; // TTL introduction time
+ }
+ int ttl_end;
+ if (!ParseIntOption(option_map_, ARG_TTL_END, ttl_end, exec_state_)) {
+ ttl_end = DBWithTTLImpl::kMaxTimestamp; // Max time allowed by TTL feature
+ }
+ if (ttl_end < ttl_start) {
+ fprintf(stderr, "Error: End time can't be less than start time\n");
+ delete iter;
+ return;
+ }
+ int time_range = ttl_end - ttl_start;
+ int bucket_size;
+ if (!ParseIntOption(option_map_, ARG_TTL_BUCKET, bucket_size, exec_state_) ||
+ bucket_size <= 0) {
+ bucket_size = time_range; // Will have just 1 bucket by default
+ }
+ // cretaing variables for row count of each type
+ std::string rtype1, rtype2, row, val;
+ rtype2 = "";
+ uint64_t c = 0;
+ uint64_t s1 = 0, s2 = 0;
+
+ // At this point, bucket_size=0 => time_range=0
+ int num_buckets = (bucket_size >= time_range)
+ ? 1
+ : ((time_range + bucket_size - 1) / bucket_size);
+ std::vector<uint64_t> bucket_counts(num_buckets, 0);
+ if (is_db_ttl_ && !count_only_ && timestamp_ && !count_delim_) {
+ fprintf(stdout, "Dumping key-values from %s to %s\n",
+ TimeToHumanString(ttl_start).c_str(),
+ TimeToHumanString(ttl_end).c_str());
+ }
+
+ HistogramImpl vsize_hist;
+
+ for (; iter->Valid(); iter->Next()) {
+ int rawtime = 0;
+ // If end marker was specified, we stop before it
+ if (!null_to_ && (iter->key().ToString() >= to_)) break;
+ // Terminate if maximum number of keys have been dumped
+ if (max_keys == 0) break;
+ if (is_db_ttl_) {
+ TtlIterator* it_ttl = static_cast_with_check<TtlIterator>(iter);
+ rawtime = it_ttl->ttl_timestamp();
+ if (rawtime < ttl_start || rawtime >= ttl_end) {
+ continue;
+ }
+ }
+ if (max_keys > 0) {
+ --max_keys;
+ }
+ if (is_db_ttl_ && num_buckets > 1) {
+ IncBucketCounts(bucket_counts, ttl_start, time_range, bucket_size,
+ rawtime, num_buckets);
+ }
+ ++count;
+ if (count_delim_) {
+ rtype1 = "";
+ row = iter->key().ToString();
+ val = iter->value().ToString();
+ s1 = row.size() + val.size();
+ for (int j = 0; row[j] != delim_[0] && row[j] != '\0'; j++)
+ rtype1 += row[j];
+ if (rtype2.compare("") && rtype2.compare(rtype1) != 0) {
+ fprintf(stdout, "%s => count:%" PRIu64 "\tsize:%" PRIu64 "\n",
+ rtype2.c_str(), c, s2);
+ c = 1;
+ s2 = s1;
+ rtype2 = rtype1;
+ } else {
+ c++;
+ s2 += s1;
+ rtype2 = rtype1;
+ }
+ }
+
+ if (count_only_) {
+ vsize_hist.Add(iter->value().size());
+ }
+
+ if (!count_only_ && !count_delim_) {
+ if (is_db_ttl_ && timestamp_) {
+ fprintf(stdout, "%s ", TimeToHumanString(rawtime).c_str());
+ }
+ std::string str =
+ PrintKeyValue(iter->key().ToString(), iter->value().ToString(),
+ is_key_hex_, is_value_hex_);
+ fprintf(stdout, "%s\n", str.c_str());
+ }
+ }
+
+ if (num_buckets > 1 && is_db_ttl_) {
+ PrintBucketCounts(bucket_counts, ttl_start, ttl_end, bucket_size,
+ num_buckets);
+ } else if (count_delim_) {
+ fprintf(stdout, "%s => count:%" PRIu64 "\tsize:%" PRIu64 "\n",
+ rtype2.c_str(), c, s2);
+ } else {
+ fprintf(stdout, "Keys in range: %" PRIu64 "\n", count);
+ }
+
+ if (count_only_) {
+ fprintf(stdout, "Value size distribution: \n");
+ fprintf(stdout, "%s\n", vsize_hist.ToString().c_str());
+ }
+ // Clean up
+ delete iter;
+}
+
+const std::string ReduceDBLevelsCommand::ARG_NEW_LEVELS = "new_levels";
+const std::string ReduceDBLevelsCommand::ARG_PRINT_OLD_LEVELS =
+ "print_old_levels";
+
+ReduceDBLevelsCommand::ReduceDBLevelsCommand(
+ const std::vector<std::string>& /*params*/,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags)
+ : LDBCommand(options, flags, false,
+ BuildCmdLineOptions({ARG_NEW_LEVELS, ARG_PRINT_OLD_LEVELS})),
+ old_levels_(1 << 7),
+ new_levels_(-1),
+ print_old_levels_(false) {
+ ParseIntOption(option_map_, ARG_NEW_LEVELS, new_levels_, exec_state_);
+ print_old_levels_ = IsFlagPresent(flags, ARG_PRINT_OLD_LEVELS);
+
+ if (new_levels_ <= 0) {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ " Use --" + ARG_NEW_LEVELS + " to specify a new level number\n");
+ }
+}
+
+std::vector<std::string> ReduceDBLevelsCommand::PrepareArgs(
+ const std::string& db_path, int new_levels, bool print_old_level) {
+ std::vector<std::string> ret;
+ ret.push_back("reduce_levels");
+ ret.push_back("--" + ARG_DB + "=" + db_path);
+ ret.push_back("--" + ARG_NEW_LEVELS + "=" + std::to_string(new_levels));
+ if (print_old_level) {
+ ret.push_back("--" + ARG_PRINT_OLD_LEVELS);
+ }
+ return ret;
+}
+
+void ReduceDBLevelsCommand::Help(std::string& ret) {
+ ret.append(" ");
+ ret.append(ReduceDBLevelsCommand::Name());
+ ret.append(" --" + ARG_NEW_LEVELS + "=<New number of levels>");
+ ret.append(" [--" + ARG_PRINT_OLD_LEVELS + "]");
+ ret.append("\n");
+}
+
+void ReduceDBLevelsCommand::OverrideBaseCFOptions(
+ ColumnFamilyOptions* cf_opts) {
+ LDBCommand::OverrideBaseCFOptions(cf_opts);
+ cf_opts->num_levels = old_levels_;
+ cf_opts->max_bytes_for_level_multiplier_additional.resize(cf_opts->num_levels,
+ 1);
+ // Disable size compaction
+ cf_opts->max_bytes_for_level_base = 1ULL << 50;
+ cf_opts->max_bytes_for_level_multiplier = 1;
+}
+
+Status ReduceDBLevelsCommand::GetOldNumOfLevels(Options& opt, int* levels) {
+ ImmutableDBOptions db_options(opt);
+ EnvOptions soptions;
+ std::shared_ptr<Cache> tc(
+ NewLRUCache(opt.max_open_files - 10, opt.table_cache_numshardbits));
+ const InternalKeyComparator cmp(opt.comparator);
+ WriteController wc(opt.delayed_write_rate);
+ WriteBufferManager wb(opt.db_write_buffer_size);
+ VersionSet versions(db_path_, &db_options, soptions, tc.get(), &wb, &wc,
+ /*block_cache_tracer=*/nullptr, /*io_tracer=*/nullptr,
+ /*db_id*/ "", /*db_session_id*/ "");
+ std::vector<ColumnFamilyDescriptor> dummy;
+ ColumnFamilyDescriptor dummy_descriptor(kDefaultColumnFamilyName,
+ ColumnFamilyOptions(opt));
+ dummy.push_back(dummy_descriptor);
+ // We rely the VersionSet::Recover to tell us the internal data structures
+ // in the db. And the Recover() should never do any change
+ // (like LogAndApply) to the manifest file.
+ Status st = versions.Recover(dummy);
+ if (!st.ok()) {
+ return st;
+ }
+ int max = -1;
+ auto default_cfd = versions.GetColumnFamilySet()->GetDefault();
+ for (int i = 0; i < default_cfd->NumberLevels(); i++) {
+ if (default_cfd->current()->storage_info()->NumLevelFiles(i)) {
+ max = i;
+ }
+ }
+
+ *levels = max + 1;
+ return st;
+}
+
+void ReduceDBLevelsCommand::DoCommand() {
+ if (new_levels_ <= 1) {
+ exec_state_ =
+ LDBCommandExecuteResult::Failed("Invalid number of levels.\n");
+ return;
+ }
+
+ Status st;
+ PrepareOptions();
+ int old_level_num = -1;
+ st = GetOldNumOfLevels(options_, &old_level_num);
+ if (!st.ok()) {
+ exec_state_ = LDBCommandExecuteResult::Failed(st.ToString());
+ return;
+ }
+
+ if (print_old_levels_) {
+ fprintf(stdout, "The old number of levels in use is %d\n", old_level_num);
+ }
+
+ if (old_level_num <= new_levels_) {
+ return;
+ }
+
+ old_levels_ = old_level_num;
+
+ OpenDB();
+ if (exec_state_.IsFailed()) {
+ return;
+ }
+ assert(db_ != nullptr);
+ // Compact the whole DB to put all files to the highest level.
+ fprintf(stdout, "Compacting the db...\n");
+ st =
+ db_->CompactRange(CompactRangeOptions(), GetCfHandle(), nullptr, nullptr);
+
+ CloseDB();
+
+ if (st.ok()) {
+ EnvOptions soptions;
+ st = VersionSet::ReduceNumberOfLevels(db_path_, &options_, soptions,
+ new_levels_);
+ }
+ if (!st.ok()) {
+ exec_state_ = LDBCommandExecuteResult::Failed(st.ToString());
+ return;
+ }
+}
+
+const std::string ChangeCompactionStyleCommand::ARG_OLD_COMPACTION_STYLE =
+ "old_compaction_style";
+const std::string ChangeCompactionStyleCommand::ARG_NEW_COMPACTION_STYLE =
+ "new_compaction_style";
+
+ChangeCompactionStyleCommand::ChangeCompactionStyleCommand(
+ const std::vector<std::string>& /*params*/,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags)
+ : LDBCommand(options, flags, false,
+ BuildCmdLineOptions(
+ {ARG_OLD_COMPACTION_STYLE, ARG_NEW_COMPACTION_STYLE})),
+ old_compaction_style_(-1),
+ new_compaction_style_(-1) {
+ ParseIntOption(option_map_, ARG_OLD_COMPACTION_STYLE, old_compaction_style_,
+ exec_state_);
+ if (old_compaction_style_ != kCompactionStyleLevel &&
+ old_compaction_style_ != kCompactionStyleUniversal) {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ "Use --" + ARG_OLD_COMPACTION_STYLE + " to specify old compaction " +
+ "style. Check ldb help for proper compaction style value.\n");
+ return;
+ }
+
+ ParseIntOption(option_map_, ARG_NEW_COMPACTION_STYLE, new_compaction_style_,
+ exec_state_);
+ if (new_compaction_style_ != kCompactionStyleLevel &&
+ new_compaction_style_ != kCompactionStyleUniversal) {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ "Use --" + ARG_NEW_COMPACTION_STYLE + " to specify new compaction " +
+ "style. Check ldb help for proper compaction style value.\n");
+ return;
+ }
+
+ if (new_compaction_style_ == old_compaction_style_) {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ "Old compaction style is the same as new compaction style. "
+ "Nothing to do.\n");
+ return;
+ }
+
+ if (old_compaction_style_ == kCompactionStyleUniversal &&
+ new_compaction_style_ == kCompactionStyleLevel) {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ "Convert from universal compaction to level compaction. "
+ "Nothing to do.\n");
+ return;
+ }
+}
+
+void ChangeCompactionStyleCommand::Help(std::string& ret) {
+ ret.append(" ");
+ ret.append(ChangeCompactionStyleCommand::Name());
+ ret.append(" --" + ARG_OLD_COMPACTION_STYLE + "=<Old compaction style: 0 " +
+ "for level compaction, 1 for universal compaction>");
+ ret.append(" --" + ARG_NEW_COMPACTION_STYLE + "=<New compaction style: 0 " +
+ "for level compaction, 1 for universal compaction>");
+ ret.append("\n");
+}
+
+void ChangeCompactionStyleCommand::OverrideBaseCFOptions(
+ ColumnFamilyOptions* cf_opts) {
+ LDBCommand::OverrideBaseCFOptions(cf_opts);
+ if (old_compaction_style_ == kCompactionStyleLevel &&
+ new_compaction_style_ == kCompactionStyleUniversal) {
+ // In order to convert from level compaction to universal compaction, we
+ // need to compact all data into a single file and move it to level 0.
+ cf_opts->disable_auto_compactions = true;
+ cf_opts->target_file_size_base = INT_MAX;
+ cf_opts->target_file_size_multiplier = 1;
+ cf_opts->max_bytes_for_level_base = INT_MAX;
+ cf_opts->max_bytes_for_level_multiplier = 1;
+ }
+}
+
+void ChangeCompactionStyleCommand::DoCommand() {
+ if (!db_) {
+ assert(GetExecuteState().IsFailed());
+ return;
+ }
+ // print db stats before we have made any change
+ std::string property;
+ std::string files_per_level;
+ for (int i = 0; i < db_->NumberLevels(GetCfHandle()); i++) {
+ db_->GetProperty(GetCfHandle(),
+ "rocksdb.num-files-at-level" + std::to_string(i),
+ &property);
+
+ // format print string
+ char buf[100];
+ snprintf(buf, sizeof(buf), "%s%s", (i ? "," : ""), property.c_str());
+ files_per_level += buf;
+ }
+ fprintf(stdout, "files per level before compaction: %s\n",
+ files_per_level.c_str());
+
+ // manual compact into a single file and move the file to level 0
+ CompactRangeOptions compact_options;
+ compact_options.change_level = true;
+ compact_options.target_level = 0;
+ Status s =
+ db_->CompactRange(compact_options, GetCfHandle(), nullptr, nullptr);
+ if (!s.ok()) {
+ std::stringstream oss;
+ oss << "Compaction failed: " << s.ToString();
+ exec_state_ = LDBCommandExecuteResult::Failed(oss.str());
+ return;
+ }
+
+ // verify compaction result
+ files_per_level = "";
+ int num_files = 0;
+ for (int i = 0; i < db_->NumberLevels(GetCfHandle()); i++) {
+ db_->GetProperty(GetCfHandle(),
+ "rocksdb.num-files-at-level" + std::to_string(i),
+ &property);
+
+ // format print string
+ char buf[100];
+ snprintf(buf, sizeof(buf), "%s%s", (i ? "," : ""), property.c_str());
+ files_per_level += buf;
+
+ num_files = atoi(property.c_str());
+
+ // level 0 should have only 1 file
+ if (i == 0 && num_files != 1) {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ "Number of db files at "
+ "level 0 after compaction is " +
+ std::to_string(num_files) + ", not 1.\n");
+ return;
+ }
+ // other levels should have no file
+ if (i > 0 && num_files != 0) {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ "Number of db files at "
+ "level " +
+ std::to_string(i) + " after compaction is " +
+ std::to_string(num_files) + ", not 0.\n");
+ return;
+ }
+ }
+
+ fprintf(stdout, "files per level after compaction: %s\n",
+ files_per_level.c_str());
+}
+
+// ----------------------------------------------------------------------------
+
+namespace {
+
+struct StdErrReporter : public log::Reader::Reporter {
+ void Corruption(size_t /*bytes*/, const Status& s) override {
+ std::cerr << "Corruption detected in log file " << s.ToString() << "\n";
+ }
+};
+
+class InMemoryHandler : public WriteBatch::Handler {
+ public:
+ InMemoryHandler(std::stringstream& row, bool print_values,
+ bool write_after_commit = false)
+ : Handler(),
+ row_(row),
+ print_values_(print_values),
+ write_after_commit_(write_after_commit) {}
+
+ void commonPutMerge(const Slice& key, const Slice& value) {
+ std::string k = LDBCommand::StringToHex(key.ToString());
+ if (print_values_) {
+ std::string v = LDBCommand::StringToHex(value.ToString());
+ row_ << k << " : ";
+ row_ << v << " ";
+ } else {
+ row_ << k << " ";
+ }
+ }
+
+ Status PutCF(uint32_t cf, const Slice& key, const Slice& value) override {
+ row_ << "PUT(" << cf << ") : ";
+ commonPutMerge(key, value);
+ return Status::OK();
+ }
+
+ Status MergeCF(uint32_t cf, const Slice& key, const Slice& value) override {
+ row_ << "MERGE(" << cf << ") : ";
+ commonPutMerge(key, value);
+ return Status::OK();
+ }
+
+ Status MarkNoop(bool) override {
+ row_ << "NOOP ";
+ return Status::OK();
+ }
+
+ Status DeleteCF(uint32_t cf, const Slice& key) override {
+ row_ << "DELETE(" << cf << ") : ";
+ row_ << LDBCommand::StringToHex(key.ToString()) << " ";
+ return Status::OK();
+ }
+
+ Status SingleDeleteCF(uint32_t cf, const Slice& key) override {
+ row_ << "SINGLE_DELETE(" << cf << ") : ";
+ row_ << LDBCommand::StringToHex(key.ToString()) << " ";
+ return Status::OK();
+ }
+
+ Status DeleteRangeCF(uint32_t cf, const Slice& begin_key,
+ const Slice& end_key) override {
+ row_ << "DELETE_RANGE(" << cf << ") : ";
+ row_ << LDBCommand::StringToHex(begin_key.ToString()) << " ";
+ row_ << LDBCommand::StringToHex(end_key.ToString()) << " ";
+ return Status::OK();
+ }
+
+ Status MarkBeginPrepare(bool unprepare) override {
+ row_ << "BEGIN_PREPARE(";
+ row_ << (unprepare ? "true" : "false") << ") ";
+ return Status::OK();
+ }
+
+ Status MarkEndPrepare(const Slice& xid) override {
+ row_ << "END_PREPARE(";
+ row_ << LDBCommand::StringToHex(xid.ToString()) << ") ";
+ return Status::OK();
+ }
+
+ Status MarkRollback(const Slice& xid) override {
+ row_ << "ROLLBACK(";
+ row_ << LDBCommand::StringToHex(xid.ToString()) << ") ";
+ return Status::OK();
+ }
+
+ Status MarkCommit(const Slice& xid) override {
+ row_ << "COMMIT(";
+ row_ << LDBCommand::StringToHex(xid.ToString()) << ") ";
+ return Status::OK();
+ }
+
+ Status MarkCommitWithTimestamp(const Slice& xid,
+ const Slice& commit_ts) override {
+ row_ << "COMMIT_WITH_TIMESTAMP(";
+ row_ << LDBCommand::StringToHex(xid.ToString()) << ", ";
+ row_ << LDBCommand::StringToHex(commit_ts.ToString()) << ") ";
+ return Status::OK();
+ }
+
+ ~InMemoryHandler() override {}
+
+ protected:
+ Handler::OptionState WriteAfterCommit() const override {
+ return write_after_commit_ ? Handler::OptionState::kEnabled
+ : Handler::OptionState::kDisabled;
+ }
+
+ private:
+ std::stringstream& row_;
+ bool print_values_;
+ bool write_after_commit_;
+};
+
+void DumpWalFile(Options options, std::string wal_file, bool print_header,
+ bool print_values, bool is_write_committed,
+ LDBCommandExecuteResult* exec_state) {
+ const auto& fs = options.env->GetFileSystem();
+ FileOptions soptions(options);
+ std::unique_ptr<SequentialFileReader> wal_file_reader;
+ Status status = SequentialFileReader::Create(
+ fs, wal_file, soptions, &wal_file_reader, nullptr /* dbg */,
+ nullptr /* rate_limiter */);
+ if (!status.ok()) {
+ if (exec_state) {
+ *exec_state = LDBCommandExecuteResult::Failed("Failed to open WAL file " +
+ status.ToString());
+ } else {
+ std::cerr << "Error: Failed to open WAL file " << status.ToString()
+ << std::endl;
+ }
+ } else {
+ StdErrReporter reporter;
+ uint64_t log_number;
+ FileType type;
+
+ // we need the log number, but ParseFilename expects dbname/NNN.log.
+ std::string sanitized = wal_file;
+ size_t lastslash = sanitized.rfind('/');
+ if (lastslash != std::string::npos)
+ sanitized = sanitized.substr(lastslash + 1);
+ if (!ParseFileName(sanitized, &log_number, &type)) {
+ // bogus input, carry on as best we can
+ log_number = 0;
+ }
+ log::Reader reader(options.info_log, std::move(wal_file_reader), &reporter,
+ true /* checksum */, log_number);
+ std::string scratch;
+ WriteBatch batch;
+ Slice record;
+ std::stringstream row;
+ if (print_header) {
+ std::cout << "Sequence,Count,ByteSize,Physical Offset,Key(s)";
+ if (print_values) {
+ std::cout << " : value ";
+ }
+ std::cout << "\n";
+ }
+ while (status.ok() && reader.ReadRecord(&record, &scratch)) {
+ row.str("");
+ if (record.size() < WriteBatchInternal::kHeader) {
+ reporter.Corruption(record.size(),
+ Status::Corruption("log record too small"));
+ } else {
+ status = WriteBatchInternal::SetContents(&batch, record);
+ if (!status.ok()) {
+ std::stringstream oss;
+ oss << "Parsing write batch failed: " << status.ToString();
+ if (exec_state) {
+ *exec_state = LDBCommandExecuteResult::Failed(oss.str());
+ } else {
+ std::cerr << oss.str() << std::endl;
+ }
+ break;
+ }
+ row << WriteBatchInternal::Sequence(&batch) << ",";
+ row << WriteBatchInternal::Count(&batch) << ",";
+ row << WriteBatchInternal::ByteSize(&batch) << ",";
+ row << reader.LastRecordOffset() << ",";
+ InMemoryHandler handler(row, print_values, is_write_committed);
+ status = batch.Iterate(&handler);
+ if (!status.ok()) {
+ if (exec_state) {
+ std::stringstream oss;
+ oss << "Print write batch error: " << status.ToString();
+ *exec_state = LDBCommandExecuteResult::Failed(oss.str());
+ }
+ row << "error: " << status.ToString();
+ break;
+ }
+ row << "\n";
+ }
+ std::cout << row.str();
+ }
+ }
+}
+
+} // namespace
+
+const std::string WALDumperCommand::ARG_WAL_FILE = "walfile";
+const std::string WALDumperCommand::ARG_WRITE_COMMITTED = "write_committed";
+const std::string WALDumperCommand::ARG_PRINT_VALUE = "print_value";
+const std::string WALDumperCommand::ARG_PRINT_HEADER = "header";
+
+WALDumperCommand::WALDumperCommand(
+ const std::vector<std::string>& /*params*/,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags)
+ : LDBCommand(options, flags, true,
+ BuildCmdLineOptions({ARG_WAL_FILE, ARG_WRITE_COMMITTED,
+ ARG_PRINT_HEADER, ARG_PRINT_VALUE})),
+ print_header_(false),
+ print_values_(false),
+ is_write_committed_(false) {
+ wal_file_.clear();
+
+ auto itr = options.find(ARG_WAL_FILE);
+ if (itr != options.end()) {
+ wal_file_ = itr->second;
+ }
+
+ print_header_ = IsFlagPresent(flags, ARG_PRINT_HEADER);
+ print_values_ = IsFlagPresent(flags, ARG_PRINT_VALUE);
+ is_write_committed_ = ParseBooleanOption(options, ARG_WRITE_COMMITTED, true);
+
+ if (wal_file_.empty()) {
+ exec_state_ = LDBCommandExecuteResult::Failed("Argument " + ARG_WAL_FILE +
+ " must be specified.");
+ }
+}
+
+void WALDumperCommand::Help(std::string& ret) {
+ ret.append(" ");
+ ret.append(WALDumperCommand::Name());
+ ret.append(" --" + ARG_WAL_FILE + "=<write_ahead_log_file_path>");
+ ret.append(" [--" + ARG_PRINT_HEADER + "] ");
+ ret.append(" [--" + ARG_PRINT_VALUE + "] ");
+ ret.append(" [--" + ARG_WRITE_COMMITTED + "=true|false] ");
+ ret.append("\n");
+}
+
+void WALDumperCommand::DoCommand() {
+ DumpWalFile(options_, wal_file_, print_header_, print_values_,
+ is_write_committed_, &exec_state_);
+}
+
+// ----------------------------------------------------------------------------
+
+GetCommand::GetCommand(const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags)
+ : LDBCommand(
+ options, flags, true,
+ BuildCmdLineOptions({ARG_TTL, ARG_HEX, ARG_KEY_HEX, ARG_VALUE_HEX})) {
+ if (params.size() != 1) {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ "<key> must be specified for the get command");
+ } else {
+ key_ = params.at(0);
+ }
+
+ if (is_key_hex_) {
+ key_ = HexToString(key_);
+ }
+}
+
+void GetCommand::Help(std::string& ret) {
+ ret.append(" ");
+ ret.append(GetCommand::Name());
+ ret.append(" <key>");
+ ret.append(" [--" + ARG_TTL + "]");
+ ret.append("\n");
+}
+
+void GetCommand::DoCommand() {
+ if (!db_) {
+ assert(GetExecuteState().IsFailed());
+ return;
+ }
+ std::string value;
+ Status st = db_->Get(ReadOptions(), GetCfHandle(), key_, &value);
+ if (st.ok()) {
+ fprintf(stdout, "%s\n",
+ (is_value_hex_ ? StringToHex(value) : value).c_str());
+ } else {
+ std::stringstream oss;
+ oss << "Get failed: " << st.ToString();
+ exec_state_ = LDBCommandExecuteResult::Failed(oss.str());
+ }
+}
+
+// ----------------------------------------------------------------------------
+
+ApproxSizeCommand::ApproxSizeCommand(
+ const std::vector<std::string>& /*params*/,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags)
+ : LDBCommand(options, flags, true,
+ BuildCmdLineOptions(
+ {ARG_HEX, ARG_KEY_HEX, ARG_VALUE_HEX, ARG_FROM, ARG_TO})) {
+ if (options.find(ARG_FROM) != options.end()) {
+ start_key_ = options.find(ARG_FROM)->second;
+ } else {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ ARG_FROM + " must be specified for approxsize command");
+ return;
+ }
+
+ if (options.find(ARG_TO) != options.end()) {
+ end_key_ = options.find(ARG_TO)->second;
+ } else {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ ARG_TO + " must be specified for approxsize command");
+ return;
+ }
+
+ if (is_key_hex_) {
+ start_key_ = HexToString(start_key_);
+ end_key_ = HexToString(end_key_);
+ }
+}
+
+void ApproxSizeCommand::Help(std::string& ret) {
+ ret.append(" ");
+ ret.append(ApproxSizeCommand::Name());
+ ret.append(HelpRangeCmdArgs());
+ ret.append("\n");
+}
+
+void ApproxSizeCommand::DoCommand() {
+ if (!db_) {
+ assert(GetExecuteState().IsFailed());
+ return;
+ }
+ Range ranges[1];
+ ranges[0] = Range(start_key_, end_key_);
+ uint64_t sizes[1];
+ Status s = db_->GetApproximateSizes(GetCfHandle(), ranges, 1, sizes);
+ if (!s.ok()) {
+ std::stringstream oss;
+ oss << "ApproximateSize failed: " << s.ToString();
+ exec_state_ = LDBCommandExecuteResult::Failed(oss.str());
+ } else {
+ fprintf(stdout, "%lu\n", (unsigned long)sizes[0]);
+ }
+}
+
+// ----------------------------------------------------------------------------
+
+BatchPutCommand::BatchPutCommand(
+ const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags)
+ : LDBCommand(options, flags, false,
+ BuildCmdLineOptions({ARG_TTL, ARG_HEX, ARG_KEY_HEX,
+ ARG_VALUE_HEX, ARG_CREATE_IF_MISSING})) {
+ if (params.size() < 2) {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ "At least one <key> <value> pair must be specified batchput.");
+ } else if (params.size() % 2 != 0) {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ "Equal number of <key>s and <value>s must be specified for batchput.");
+ } else {
+ for (size_t i = 0; i < params.size(); i += 2) {
+ std::string key = params.at(i);
+ std::string value = params.at(i + 1);
+ key_values_.push_back(std::pair<std::string, std::string>(
+ is_key_hex_ ? HexToString(key) : key,
+ is_value_hex_ ? HexToString(value) : value));
+ }
+ }
+ create_if_missing_ = IsFlagPresent(flags_, ARG_CREATE_IF_MISSING);
+}
+
+void BatchPutCommand::Help(std::string& ret) {
+ ret.append(" ");
+ ret.append(BatchPutCommand::Name());
+ ret.append(" <key> <value> [<key> <value>] [..]");
+ ret.append(" [--" + ARG_CREATE_IF_MISSING + "]");
+ ret.append(" [--" + ARG_TTL + "]");
+ ret.append("\n");
+}
+
+void BatchPutCommand::DoCommand() {
+ if (!db_) {
+ assert(GetExecuteState().IsFailed());
+ return;
+ }
+ WriteBatch batch;
+
+ Status st;
+ std::stringstream oss;
+ for (std::vector<std::pair<std::string, std::string>>::const_iterator itr =
+ key_values_.begin();
+ itr != key_values_.end(); ++itr) {
+ st = batch.Put(GetCfHandle(), itr->first, itr->second);
+ if (!st.ok()) {
+ oss << "Put to write batch failed: " << itr->first << "=>" << itr->second
+ << " error: " << st.ToString();
+ break;
+ }
+ }
+ if (st.ok()) {
+ st = db_->Write(WriteOptions(), &batch);
+ if (!st.ok()) {
+ oss << "Write failed: " << st.ToString();
+ }
+ }
+ if (st.ok()) {
+ fprintf(stdout, "OK\n");
+ } else {
+ exec_state_ = LDBCommandExecuteResult::Failed(oss.str());
+ }
+}
+
+void BatchPutCommand::OverrideBaseOptions() {
+ LDBCommand::OverrideBaseOptions();
+ options_.create_if_missing = create_if_missing_;
+}
+
+// ----------------------------------------------------------------------------
+
+ScanCommand::ScanCommand(const std::vector<std::string>& /*params*/,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags)
+ : LDBCommand(
+ options, flags, true,
+ BuildCmdLineOptions({ARG_TTL, ARG_NO_VALUE, ARG_HEX, ARG_KEY_HEX,
+ ARG_TO, ARG_VALUE_HEX, ARG_FROM, ARG_TIMESTAMP,
+ ARG_MAX_KEYS, ARG_TTL_START, ARG_TTL_END})),
+ start_key_specified_(false),
+ end_key_specified_(false),
+ max_keys_scanned_(-1),
+ no_value_(false) {
+ auto itr = options.find(ARG_FROM);
+ if (itr != options.end()) {
+ start_key_ = itr->second;
+ if (is_key_hex_) {
+ start_key_ = HexToString(start_key_);
+ }
+ start_key_specified_ = true;
+ }
+ itr = options.find(ARG_TO);
+ if (itr != options.end()) {
+ end_key_ = itr->second;
+ if (is_key_hex_) {
+ end_key_ = HexToString(end_key_);
+ }
+ end_key_specified_ = true;
+ }
+
+ std::vector<std::string>::const_iterator vitr =
+ std::find(flags.begin(), flags.end(), ARG_NO_VALUE);
+ if (vitr != flags.end()) {
+ no_value_ = true;
+ }
+
+ itr = options.find(ARG_MAX_KEYS);
+ if (itr != options.end()) {
+ try {
+#if defined(CYGWIN)
+ max_keys_scanned_ = strtol(itr->second.c_str(), 0, 10);
+#else
+ max_keys_scanned_ = std::stoi(itr->second);
+#endif
+ } catch (const std::invalid_argument&) {
+ exec_state_ = LDBCommandExecuteResult::Failed(ARG_MAX_KEYS +
+ " has an invalid value");
+ } catch (const std::out_of_range&) {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ ARG_MAX_KEYS + " has a value out-of-range");
+ }
+ }
+}
+
+void ScanCommand::Help(std::string& ret) {
+ ret.append(" ");
+ ret.append(ScanCommand::Name());
+ ret.append(HelpRangeCmdArgs());
+ ret.append(" [--" + ARG_TTL + "]");
+ ret.append(" [--" + ARG_TIMESTAMP + "]");
+ ret.append(" [--" + ARG_MAX_KEYS + "=<N>q] ");
+ ret.append(" [--" + ARG_TTL_START + "=<N>:- is inclusive]");
+ ret.append(" [--" + ARG_TTL_END + "=<N>:- is exclusive]");
+ ret.append(" [--" + ARG_NO_VALUE + "]");
+ ret.append("\n");
+}
+
+void ScanCommand::DoCommand() {
+ if (!db_) {
+ assert(GetExecuteState().IsFailed());
+ return;
+ }
+
+ int num_keys_scanned = 0;
+ ReadOptions scan_read_opts;
+ scan_read_opts.total_order_seek = true;
+ Iterator* it = db_->NewIterator(scan_read_opts, GetCfHandle());
+ if (start_key_specified_) {
+ it->Seek(start_key_);
+ } else {
+ it->SeekToFirst();
+ }
+ int ttl_start;
+ if (!ParseIntOption(option_map_, ARG_TTL_START, ttl_start, exec_state_)) {
+ ttl_start = DBWithTTLImpl::kMinTimestamp; // TTL introduction time
+ }
+ int ttl_end;
+ if (!ParseIntOption(option_map_, ARG_TTL_END, ttl_end, exec_state_)) {
+ ttl_end = DBWithTTLImpl::kMaxTimestamp; // Max time allowed by TTL feature
+ }
+ if (ttl_end < ttl_start) {
+ fprintf(stderr, "Error: End time can't be less than start time\n");
+ delete it;
+ return;
+ }
+ if (is_db_ttl_ && timestamp_) {
+ fprintf(stdout, "Scanning key-values from %s to %s\n",
+ TimeToHumanString(ttl_start).c_str(),
+ TimeToHumanString(ttl_end).c_str());
+ }
+ for (;
+ it->Valid() && (!end_key_specified_ || it->key().ToString() < end_key_);
+ it->Next()) {
+ if (is_db_ttl_) {
+ TtlIterator* it_ttl = static_cast_with_check<TtlIterator>(it);
+ int rawtime = it_ttl->ttl_timestamp();
+ if (rawtime < ttl_start || rawtime >= ttl_end) {
+ continue;
+ }
+ if (timestamp_) {
+ fprintf(stdout, "%s ", TimeToHumanString(rawtime).c_str());
+ }
+ }
+
+ Slice key_slice = it->key();
+
+ std::string formatted_key;
+ if (is_key_hex_) {
+ formatted_key = "0x" + key_slice.ToString(true /* hex */);
+ key_slice = formatted_key;
+ } else if (ldb_options_.key_formatter) {
+ formatted_key = ldb_options_.key_formatter->Format(key_slice);
+ key_slice = formatted_key;
+ }
+
+ if (no_value_) {
+ fprintf(stdout, "%.*s\n", static_cast<int>(key_slice.size()),
+ key_slice.data());
+ } else {
+ Slice val_slice = it->value();
+ std::string formatted_value;
+ if (is_value_hex_) {
+ formatted_value = "0x" + val_slice.ToString(true /* hex */);
+ val_slice = formatted_value;
+ }
+ fprintf(stdout, "%.*s : %.*s\n", static_cast<int>(key_slice.size()),
+ key_slice.data(), static_cast<int>(val_slice.size()),
+ val_slice.data());
+ }
+
+ num_keys_scanned++;
+ if (max_keys_scanned_ >= 0 && num_keys_scanned >= max_keys_scanned_) {
+ break;
+ }
+ }
+ if (!it->status().ok()) { // Check for any errors found during the scan
+ exec_state_ = LDBCommandExecuteResult::Failed(it->status().ToString());
+ }
+ delete it;
+}
+
+// ----------------------------------------------------------------------------
+
+DeleteCommand::DeleteCommand(const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags)
+ : LDBCommand(options, flags, false,
+ BuildCmdLineOptions({ARG_HEX, ARG_KEY_HEX, ARG_VALUE_HEX})) {
+ if (params.size() != 1) {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ "KEY must be specified for the delete command");
+ } else {
+ key_ = params.at(0);
+ if (is_key_hex_) {
+ key_ = HexToString(key_);
+ }
+ }
+}
+
+void DeleteCommand::Help(std::string& ret) {
+ ret.append(" ");
+ ret.append(DeleteCommand::Name() + " <key>");
+ ret.append("\n");
+}
+
+void DeleteCommand::DoCommand() {
+ if (!db_) {
+ assert(GetExecuteState().IsFailed());
+ return;
+ }
+ Status st = db_->Delete(WriteOptions(), GetCfHandle(), key_);
+ if (st.ok()) {
+ fprintf(stdout, "OK\n");
+ } else {
+ exec_state_ = LDBCommandExecuteResult::Failed(st.ToString());
+ }
+}
+
+SingleDeleteCommand::SingleDeleteCommand(
+ const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags)
+ : LDBCommand(options, flags, false,
+ BuildCmdLineOptions({ARG_HEX, ARG_KEY_HEX, ARG_VALUE_HEX})) {
+ if (params.size() != 1) {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ "KEY must be specified for the single delete command");
+ } else {
+ key_ = params.at(0);
+ if (is_key_hex_) {
+ key_ = HexToString(key_);
+ }
+ }
+}
+
+void SingleDeleteCommand::Help(std::string& ret) {
+ ret.append(" ");
+ ret.append(SingleDeleteCommand::Name() + " <key>");
+ ret.append("\n");
+}
+
+void SingleDeleteCommand::DoCommand() {
+ if (!db_) {
+ assert(GetExecuteState().IsFailed());
+ return;
+ }
+ Status st = db_->SingleDelete(WriteOptions(), GetCfHandle(), key_);
+ if (st.ok()) {
+ fprintf(stdout, "OK\n");
+ } else {
+ exec_state_ = LDBCommandExecuteResult::Failed(st.ToString());
+ }
+}
+
+DeleteRangeCommand::DeleteRangeCommand(
+ const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags)
+ : LDBCommand(options, flags, false,
+ BuildCmdLineOptions({ARG_HEX, ARG_KEY_HEX, ARG_VALUE_HEX})) {
+ if (params.size() != 2) {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ "begin and end keys must be specified for the delete command");
+ } else {
+ begin_key_ = params.at(0);
+ end_key_ = params.at(1);
+ if (is_key_hex_) {
+ begin_key_ = HexToString(begin_key_);
+ end_key_ = HexToString(end_key_);
+ }
+ }
+}
+
+void DeleteRangeCommand::Help(std::string& ret) {
+ ret.append(" ");
+ ret.append(DeleteRangeCommand::Name() + " <begin key> <end key>");
+ ret.append("\n");
+}
+
+void DeleteRangeCommand::DoCommand() {
+ if (!db_) {
+ assert(GetExecuteState().IsFailed());
+ return;
+ }
+ Status st =
+ db_->DeleteRange(WriteOptions(), GetCfHandle(), begin_key_, end_key_);
+ if (st.ok()) {
+ fprintf(stdout, "OK\n");
+ } else {
+ exec_state_ = LDBCommandExecuteResult::Failed(st.ToString());
+ }
+}
+
+PutCommand::PutCommand(const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags)
+ : LDBCommand(options, flags, false,
+ BuildCmdLineOptions({ARG_TTL, ARG_HEX, ARG_KEY_HEX,
+ ARG_VALUE_HEX, ARG_CREATE_IF_MISSING})) {
+ if (params.size() != 2) {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ "<key> and <value> must be specified for the put command");
+ } else {
+ key_ = params.at(0);
+ value_ = params.at(1);
+ }
+
+ if (is_key_hex_) {
+ key_ = HexToString(key_);
+ }
+
+ if (is_value_hex_) {
+ value_ = HexToString(value_);
+ }
+ create_if_missing_ = IsFlagPresent(flags_, ARG_CREATE_IF_MISSING);
+}
+
+void PutCommand::Help(std::string& ret) {
+ ret.append(" ");
+ ret.append(PutCommand::Name());
+ ret.append(" <key> <value>");
+ ret.append(" [--" + ARG_CREATE_IF_MISSING + "]");
+ ret.append(" [--" + ARG_TTL + "]");
+ ret.append("\n");
+}
+
+void PutCommand::DoCommand() {
+ if (!db_) {
+ assert(GetExecuteState().IsFailed());
+ return;
+ }
+ Status st = db_->Put(WriteOptions(), GetCfHandle(), key_, value_);
+ if (st.ok()) {
+ fprintf(stdout, "OK\n");
+ } else {
+ exec_state_ = LDBCommandExecuteResult::Failed(st.ToString());
+ }
+}
+
+void PutCommand::OverrideBaseOptions() {
+ LDBCommand::OverrideBaseOptions();
+ options_.create_if_missing = create_if_missing_;
+}
+
+// ----------------------------------------------------------------------------
+
+const char* DBQuerierCommand::HELP_CMD = "help";
+const char* DBQuerierCommand::GET_CMD = "get";
+const char* DBQuerierCommand::PUT_CMD = "put";
+const char* DBQuerierCommand::DELETE_CMD = "delete";
+
+DBQuerierCommand::DBQuerierCommand(
+ const std::vector<std::string>& /*params*/,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags)
+ : LDBCommand(
+ options, flags, false,
+ BuildCmdLineOptions({ARG_TTL, ARG_HEX, ARG_KEY_HEX, ARG_VALUE_HEX})) {
+
+}
+
+void DBQuerierCommand::Help(std::string& ret) {
+ ret.append(" ");
+ ret.append(DBQuerierCommand::Name());
+ ret.append(" [--" + ARG_TTL + "]");
+ ret.append("\n");
+ ret.append(
+ " Starts a REPL shell. Type help for list of available "
+ "commands.");
+ ret.append("\n");
+}
+
+void DBQuerierCommand::DoCommand() {
+ if (!db_) {
+ assert(GetExecuteState().IsFailed());
+ return;
+ }
+
+ ReadOptions read_options;
+ WriteOptions write_options;
+
+ std::string line;
+ std::string key;
+ std::string value;
+ Status s;
+ std::stringstream oss;
+ while (s.ok() && getline(std::cin, line, '\n')) {
+ // Parse line into std::vector<std::string>
+ std::vector<std::string> tokens;
+ size_t pos = 0;
+ while (true) {
+ size_t pos2 = line.find(' ', pos);
+ if (pos2 == std::string::npos) {
+ break;
+ }
+ tokens.push_back(line.substr(pos, pos2 - pos));
+ pos = pos2 + 1;
+ }
+ tokens.push_back(line.substr(pos));
+
+ const std::string& cmd = tokens[0];
+
+ if (cmd == HELP_CMD) {
+ fprintf(stdout,
+ "get <key>\n"
+ "put <key> <value>\n"
+ "delete <key>\n");
+ } else if (cmd == DELETE_CMD && tokens.size() == 2) {
+ key = (is_key_hex_ ? HexToString(tokens[1]) : tokens[1]);
+ s = db_->Delete(write_options, GetCfHandle(), Slice(key));
+ if (s.ok()) {
+ fprintf(stdout, "Successfully deleted %s\n", tokens[1].c_str());
+ } else {
+ oss << "delete " << key << " failed: " << s.ToString();
+ }
+ } else if (cmd == PUT_CMD && tokens.size() == 3) {
+ key = (is_key_hex_ ? HexToString(tokens[1]) : tokens[1]);
+ value = (is_value_hex_ ? HexToString(tokens[2]) : tokens[2]);
+ s = db_->Put(write_options, GetCfHandle(), Slice(key), Slice(value));
+ if (s.ok()) {
+ fprintf(stdout, "Successfully put %s %s\n", tokens[1].c_str(),
+ tokens[2].c_str());
+ } else {
+ oss << "put " << key << "=>" << value << " failed: " << s.ToString();
+ }
+ } else if (cmd == GET_CMD && tokens.size() == 2) {
+ key = (is_key_hex_ ? HexToString(tokens[1]) : tokens[1]);
+ s = db_->Get(read_options, GetCfHandle(), Slice(key), &value);
+ if (s.ok()) {
+ fprintf(stdout, "%s\n",
+ PrintKeyValue(key, value, is_key_hex_, is_value_hex_).c_str());
+ } else {
+ if (s.IsNotFound()) {
+ fprintf(stdout, "Not found %s\n", tokens[1].c_str());
+ } else {
+ oss << "get " << key << " error: " << s.ToString();
+ }
+ }
+ } else {
+ fprintf(stdout, "Unknown command %s\n", line.c_str());
+ }
+ }
+ if (!s.ok()) {
+ exec_state_ = LDBCommandExecuteResult::Failed(oss.str());
+ }
+}
+
+// ----------------------------------------------------------------------------
+
+CheckConsistencyCommand::CheckConsistencyCommand(
+ const std::vector<std::string>& /*params*/,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags)
+ : LDBCommand(options, flags, true, BuildCmdLineOptions({})) {}
+
+void CheckConsistencyCommand::Help(std::string& ret) {
+ ret.append(" ");
+ ret.append(CheckConsistencyCommand::Name());
+ ret.append("\n");
+}
+
+void CheckConsistencyCommand::DoCommand() {
+ options_.paranoid_checks = true;
+ options_.num_levels = 64;
+ OpenDB();
+ if (exec_state_.IsSucceed() || exec_state_.IsNotStarted()) {
+ fprintf(stdout, "OK\n");
+ }
+ CloseDB();
+}
+
+// ----------------------------------------------------------------------------
+
+const std::string CheckPointCommand::ARG_CHECKPOINT_DIR = "checkpoint_dir";
+
+CheckPointCommand::CheckPointCommand(
+ const std::vector<std::string>& /*params*/,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags)
+ : LDBCommand(options, flags, false /* is_read_only */,
+ BuildCmdLineOptions({ARG_CHECKPOINT_DIR})) {
+ auto itr = options.find(ARG_CHECKPOINT_DIR);
+ if (itr != options.end()) {
+ checkpoint_dir_ = itr->second;
+ }
+}
+
+void CheckPointCommand::Help(std::string& ret) {
+ ret.append(" ");
+ ret.append(CheckPointCommand::Name());
+ ret.append(" [--" + ARG_CHECKPOINT_DIR + "] ");
+ ret.append("\n");
+}
+
+void CheckPointCommand::DoCommand() {
+ if (!db_) {
+ assert(GetExecuteState().IsFailed());
+ return;
+ }
+ Checkpoint* checkpoint;
+ Status status = Checkpoint::Create(db_, &checkpoint);
+ status = checkpoint->CreateCheckpoint(checkpoint_dir_);
+ if (status.ok()) {
+ fprintf(stdout, "OK\n");
+ } else {
+ exec_state_ = LDBCommandExecuteResult::Failed(status.ToString());
+ }
+}
+
+// ----------------------------------------------------------------------------
+
+const std::string RepairCommand::ARG_VERBOSE = "verbose";
+
+RepairCommand::RepairCommand(const std::vector<std::string>& /*params*/,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags)
+ : LDBCommand(options, flags, false, BuildCmdLineOptions({ARG_VERBOSE})) {
+ verbose_ = IsFlagPresent(flags, ARG_VERBOSE);
+}
+
+void RepairCommand::Help(std::string& ret) {
+ ret.append(" ");
+ ret.append(RepairCommand::Name());
+ ret.append(" [--" + ARG_VERBOSE + "]");
+ ret.append("\n");
+}
+
+void RepairCommand::OverrideBaseOptions() {
+ LDBCommand::OverrideBaseOptions();
+ auto level = verbose_ ? InfoLogLevel::INFO_LEVEL : InfoLogLevel::WARN_LEVEL;
+ options_.info_log.reset(new StderrLogger(level));
+}
+
+void RepairCommand::DoCommand() {
+ PrepareOptions();
+ Status status = RepairDB(db_path_, options_);
+ if (status.ok()) {
+ fprintf(stdout, "OK\n");
+ } else {
+ exec_state_ = LDBCommandExecuteResult::Failed(status.ToString());
+ }
+}
+
+// ----------------------------------------------------------------------------
+
+const std::string BackupEngineCommand::ARG_NUM_THREADS = "num_threads";
+const std::string BackupEngineCommand::ARG_BACKUP_ENV_URI = "backup_env_uri";
+const std::string BackupEngineCommand::ARG_BACKUP_FS_URI = "backup_fs_uri";
+const std::string BackupEngineCommand::ARG_BACKUP_DIR = "backup_dir";
+const std::string BackupEngineCommand::ARG_STDERR_LOG_LEVEL =
+ "stderr_log_level";
+
+BackupEngineCommand::BackupEngineCommand(
+ const std::vector<std::string>& /*params*/,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags)
+ : LDBCommand(options, flags, false /* is_read_only */,
+ BuildCmdLineOptions({ARG_BACKUP_ENV_URI, ARG_BACKUP_FS_URI,
+ ARG_BACKUP_DIR, ARG_NUM_THREADS,
+ ARG_STDERR_LOG_LEVEL})),
+ num_threads_(1) {
+ auto itr = options.find(ARG_NUM_THREADS);
+ if (itr != options.end()) {
+ num_threads_ = std::stoi(itr->second);
+ }
+ itr = options.find(ARG_BACKUP_ENV_URI);
+ if (itr != options.end()) {
+ backup_env_uri_ = itr->second;
+ }
+ itr = options.find(ARG_BACKUP_FS_URI);
+ if (itr != options.end()) {
+ backup_fs_uri_ = itr->second;
+ }
+ if (!backup_env_uri_.empty() && !backup_fs_uri_.empty()) {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ "you may not specity both --" + ARG_BACKUP_ENV_URI + " and --" +
+ ARG_BACKUP_FS_URI);
+ }
+ itr = options.find(ARG_BACKUP_DIR);
+ if (itr == options.end()) {
+ exec_state_ = LDBCommandExecuteResult::Failed("--" + ARG_BACKUP_DIR +
+ ": missing backup directory");
+ } else {
+ backup_dir_ = itr->second;
+ }
+
+ itr = options.find(ARG_STDERR_LOG_LEVEL);
+ if (itr != options.end()) {
+ int stderr_log_level = std::stoi(itr->second);
+ if (stderr_log_level < 0 ||
+ stderr_log_level >= InfoLogLevel::NUM_INFO_LOG_LEVELS) {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ ARG_STDERR_LOG_LEVEL + " must be >= 0 and < " +
+ std::to_string(InfoLogLevel::NUM_INFO_LOG_LEVELS) + ".");
+ } else {
+ logger_.reset(
+ new StderrLogger(static_cast<InfoLogLevel>(stderr_log_level)));
+ }
+ }
+}
+
+void BackupEngineCommand::Help(const std::string& name, std::string& ret) {
+ ret.append(" ");
+ ret.append(name);
+ ret.append(" [--" + ARG_BACKUP_ENV_URI + " | --" + ARG_BACKUP_FS_URI + "] ");
+ ret.append(" [--" + ARG_BACKUP_DIR + "] ");
+ ret.append(" [--" + ARG_NUM_THREADS + "] ");
+ ret.append(" [--" + ARG_STDERR_LOG_LEVEL + "=<int (InfoLogLevel)>] ");
+ ret.append("\n");
+}
+
+// ----------------------------------------------------------------------------
+
+BackupCommand::BackupCommand(const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags)
+ : BackupEngineCommand(params, options, flags) {}
+
+void BackupCommand::Help(std::string& ret) {
+ BackupEngineCommand::Help(Name(), ret);
+}
+
+void BackupCommand::DoCommand() {
+ BackupEngine* backup_engine;
+ Status status;
+ if (!db_) {
+ assert(GetExecuteState().IsFailed());
+ return;
+ }
+ fprintf(stdout, "open db OK\n");
+
+ Env* custom_env = backup_env_guard_.get();
+ if (custom_env == nullptr) {
+ Status s =
+ Env::CreateFromUri(config_options_, backup_env_uri_, backup_fs_uri_,
+ &custom_env, &backup_env_guard_);
+ if (!s.ok()) {
+ exec_state_ = LDBCommandExecuteResult::Failed(s.ToString());
+ return;
+ }
+ }
+ assert(custom_env != nullptr);
+
+ BackupEngineOptions backup_options =
+ BackupEngineOptions(backup_dir_, custom_env);
+ backup_options.info_log = logger_.get();
+ backup_options.max_background_operations = num_threads_;
+ status = BackupEngine::Open(options_.env, backup_options, &backup_engine);
+ if (status.ok()) {
+ fprintf(stdout, "open backup engine OK\n");
+ } else {
+ exec_state_ = LDBCommandExecuteResult::Failed(status.ToString());
+ return;
+ }
+ status = backup_engine->CreateNewBackup(db_);
+ if (status.ok()) {
+ fprintf(stdout, "create new backup OK\n");
+ } else {
+ exec_state_ = LDBCommandExecuteResult::Failed(status.ToString());
+ return;
+ }
+}
+
+// ----------------------------------------------------------------------------
+
+RestoreCommand::RestoreCommand(
+ const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags)
+ : BackupEngineCommand(params, options, flags) {}
+
+void RestoreCommand::Help(std::string& ret) {
+ BackupEngineCommand::Help(Name(), ret);
+}
+
+void RestoreCommand::DoCommand() {
+ Env* custom_env = backup_env_guard_.get();
+ if (custom_env == nullptr) {
+ Status s =
+ Env::CreateFromUri(config_options_, backup_env_uri_, backup_fs_uri_,
+ &custom_env, &backup_env_guard_);
+ if (!s.ok()) {
+ exec_state_ = LDBCommandExecuteResult::Failed(s.ToString());
+ return;
+ }
+ }
+ assert(custom_env != nullptr);
+
+ std::unique_ptr<BackupEngineReadOnly> restore_engine;
+ Status status;
+ {
+ BackupEngineOptions opts(backup_dir_, custom_env);
+ opts.info_log = logger_.get();
+ opts.max_background_operations = num_threads_;
+ BackupEngineReadOnly* raw_restore_engine_ptr;
+ status =
+ BackupEngineReadOnly::Open(options_.env, opts, &raw_restore_engine_ptr);
+ if (status.ok()) {
+ restore_engine.reset(raw_restore_engine_ptr);
+ }
+ }
+ if (status.ok()) {
+ fprintf(stdout, "open restore engine OK\n");
+ status = restore_engine->RestoreDBFromLatestBackup(db_path_, db_path_);
+ }
+ if (status.ok()) {
+ fprintf(stdout, "restore from backup OK\n");
+ } else {
+ exec_state_ = LDBCommandExecuteResult::Failed(status.ToString());
+ }
+}
+
+// ----------------------------------------------------------------------------
+
+namespace {
+
+void DumpSstFile(Options options, std::string filename, bool output_hex,
+ bool show_properties, bool decode_blob_index,
+ std::string from_key, std::string to_key) {
+ if (filename.length() <= 4 ||
+ filename.rfind(".sst") != filename.length() - 4) {
+ std::cout << "Invalid sst file name." << std::endl;
+ return;
+ }
+ // no verification
+ ROCKSDB_NAMESPACE::SstFileDumper dumper(
+ options, filename, Temperature::kUnknown,
+ 2 * 1024 * 1024 /* readahead_size */,
+ /* verify_checksum */ false, output_hex, decode_blob_index);
+ Status st = dumper.ReadSequential(true, std::numeric_limits<uint64_t>::max(),
+ !from_key.empty(), from_key,
+ !to_key.empty(), to_key);
+ if (!st.ok()) {
+ std::cerr << "Error in reading SST file " << filename << st.ToString()
+ << std::endl;
+ return;
+ }
+
+ if (show_properties) {
+ const ROCKSDB_NAMESPACE::TableProperties* table_properties;
+
+ std::shared_ptr<const ROCKSDB_NAMESPACE::TableProperties>
+ table_properties_from_reader;
+ st = dumper.ReadTableProperties(&table_properties_from_reader);
+ if (!st.ok()) {
+ std::cerr << filename << ": " << st.ToString()
+ << ". Try to use initial table properties" << std::endl;
+ table_properties = dumper.GetInitTableProperties();
+ } else {
+ table_properties = table_properties_from_reader.get();
+ }
+ if (table_properties != nullptr) {
+ std::cout << std::endl << "Table Properties:" << std::endl;
+ std::cout << table_properties->ToString("\n") << std::endl;
+ }
+ }
+}
+
+void DumpBlobFile(const std::string& filename, bool is_key_hex,
+ bool is_value_hex, bool dump_uncompressed_blobs) {
+ using ROCKSDB_NAMESPACE::blob_db::BlobDumpTool;
+ BlobDumpTool tool;
+ BlobDumpTool::DisplayType blob_type = is_value_hex
+ ? BlobDumpTool::DisplayType::kHex
+ : BlobDumpTool::DisplayType::kRaw;
+ BlobDumpTool::DisplayType show_uncompressed_blob =
+ dump_uncompressed_blobs ? blob_type : BlobDumpTool::DisplayType::kNone;
+ BlobDumpTool::DisplayType show_blob =
+ dump_uncompressed_blobs ? BlobDumpTool::DisplayType::kNone : blob_type;
+
+ BlobDumpTool::DisplayType show_key = is_key_hex
+ ? BlobDumpTool::DisplayType::kHex
+ : BlobDumpTool::DisplayType::kRaw;
+ Status s = tool.Run(filename, show_key, show_blob, show_uncompressed_blob,
+ /* show_summary */ true);
+ if (!s.ok()) {
+ fprintf(stderr, "Failed: %s\n", s.ToString().c_str());
+ }
+}
+} // namespace
+
+DBFileDumperCommand::DBFileDumperCommand(
+ const std::vector<std::string>& /*params*/,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags)
+ : LDBCommand(options, flags, true,
+ BuildCmdLineOptions(
+ {ARG_DECODE_BLOB_INDEX, ARG_DUMP_UNCOMPRESSED_BLOBS})),
+ decode_blob_index_(IsFlagPresent(flags, ARG_DECODE_BLOB_INDEX)),
+ dump_uncompressed_blobs_(
+ IsFlagPresent(flags, ARG_DUMP_UNCOMPRESSED_BLOBS)) {}
+
+void DBFileDumperCommand::Help(std::string& ret) {
+ ret.append(" ");
+ ret.append(DBFileDumperCommand::Name());
+ ret.append(" [--" + ARG_DECODE_BLOB_INDEX + "] ");
+ ret.append(" [--" + ARG_DUMP_UNCOMPRESSED_BLOBS + "] ");
+ ret.append("\n");
+}
+
+void DBFileDumperCommand::DoCommand() {
+ if (!db_) {
+ assert(GetExecuteState().IsFailed());
+ return;
+ }
+ Status s;
+
+ // TODO: Use --hex, --key_hex, --value_hex flags consistently for
+ // dumping manifest file, sst files and blob files.
+ std::cout << "Manifest File" << std::endl;
+ std::cout << "==============================" << std::endl;
+ std::string manifest_filename;
+ s = ReadFileToString(db_->GetEnv(), CurrentFileName(db_->GetName()),
+ &manifest_filename);
+ if (!s.ok() || manifest_filename.empty() ||
+ manifest_filename.back() != '\n') {
+ std::cerr << "Error when reading CURRENT file "
+ << CurrentFileName(db_->GetName()) << std::endl;
+ }
+ // remove the trailing '\n'
+ manifest_filename.resize(manifest_filename.size() - 1);
+ std::string manifest_filepath = db_->GetName() + "/" + manifest_filename;
+ // Correct concatenation of filepath and filename:
+ // Check that there is no double slashes (or more!) when concatenation
+ // happens.
+ manifest_filepath = NormalizePath(manifest_filepath);
+
+ std::cout << manifest_filepath << std::endl;
+ DumpManifestFile(options_, manifest_filepath, false, false, false);
+ std::cout << std::endl;
+
+ std::vector<ColumnFamilyMetaData> column_families;
+ db_->GetAllColumnFamilyMetaData(&column_families);
+ for (const auto& column_family : column_families) {
+ std::cout << "Column family name: " << column_family.name << std::endl;
+ std::cout << "==============================" << std::endl;
+ std::cout << std::endl;
+ std::cout << "SST Files" << std::endl;
+ std::cout << "==============================" << std::endl;
+ for (const LevelMetaData& level : column_family.levels) {
+ for (const SstFileMetaData& sst_file : level.files) {
+ std::string filename = sst_file.db_path + "/" + sst_file.name;
+ // Correct concatenation of filepath and filename:
+ // Check that there is no double slashes (or more!) when concatenation
+ // happens.
+ filename = NormalizePath(filename);
+ std::cout << filename << " level:" << level.level << std::endl;
+ std::cout << "------------------------------" << std::endl;
+ DumpSstFile(options_, filename, false, true, decode_blob_index_);
+ std::cout << std::endl;
+ }
+ }
+ std::cout << "Blob Files" << std::endl;
+ std::cout << "==============================" << std::endl;
+ for (const BlobMetaData& blob_file : column_family.blob_files) {
+ std::string filename =
+ blob_file.blob_file_path + "/" + blob_file.blob_file_name;
+ // Correct concatenation of filepath and filename:
+ // Check that there is no double slashes (or more!) when concatenation
+ // happens.
+ filename = NormalizePath(filename);
+ std::cout << filename << std::endl;
+ std::cout << "------------------------------" << std::endl;
+ DumpBlobFile(filename, /* is_key_hex */ false, /* is_value_hex */ false,
+ dump_uncompressed_blobs_);
+ std::cout << std::endl;
+ }
+ }
+ std::cout << std::endl;
+
+ std::cout << "Write Ahead Log Files" << std::endl;
+ std::cout << "==============================" << std::endl;
+ ROCKSDB_NAMESPACE::VectorLogPtr wal_files;
+ s = db_->GetSortedWalFiles(wal_files);
+ if (!s.ok()) {
+ std::cerr << "Error when getting WAL files" << std::endl;
+ } else {
+ std::string wal_dir;
+ if (options_.wal_dir.empty()) {
+ wal_dir = db_->GetName();
+ } else {
+ wal_dir = NormalizePath(options_.wal_dir + "/");
+ }
+ for (auto& wal : wal_files) {
+ // TODO(qyang): option.wal_dir should be passed into ldb command
+ std::string filename = wal_dir + wal->PathName();
+ std::cout << filename << std::endl;
+ // TODO(myabandeh): allow configuring is_write_commited
+ DumpWalFile(options_, filename, true, true, true /* is_write_commited */,
+ &exec_state_);
+ }
+ }
+}
+
+const std::string DBLiveFilesMetadataDumperCommand::ARG_SORT_BY_FILENAME =
+ "sort_by_filename";
+
+DBLiveFilesMetadataDumperCommand::DBLiveFilesMetadataDumperCommand(
+ const std::vector<std::string>& /*params*/,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags)
+ : LDBCommand(options, flags, true,
+ BuildCmdLineOptions({ARG_SORT_BY_FILENAME})) {
+ sort_by_filename_ = IsFlagPresent(flags, ARG_SORT_BY_FILENAME);
+}
+
+void DBLiveFilesMetadataDumperCommand::Help(std::string& ret) {
+ ret.append(" ");
+ ret.append(DBLiveFilesMetadataDumperCommand::Name());
+ ret.append(" [--" + ARG_SORT_BY_FILENAME + "] ");
+ ret.append("\n");
+}
+
+void DBLiveFilesMetadataDumperCommand::DoCommand() {
+ if (!db_) {
+ assert(GetExecuteState().IsFailed());
+ return;
+ }
+ Status s;
+
+ std::vector<ColumnFamilyMetaData> metadata;
+ db_->GetAllColumnFamilyMetaData(&metadata);
+ if (sort_by_filename_) {
+ std::cout << "Live SST and Blob Files:" << std::endl;
+ // tuple of <file path, level, column family name>
+ std::vector<std::tuple<std::string, int, std::string>> all_files;
+
+ for (const auto& column_metadata : metadata) {
+ // Iterate Levels
+ const auto& levels = column_metadata.levels;
+ const std::string& cf = column_metadata.name;
+ for (const auto& level_metadata : levels) {
+ // Iterate SST files
+ const auto& sst_files = level_metadata.files;
+ int level = level_metadata.level;
+ for (const auto& sst_metadata : sst_files) {
+ // The SstFileMetaData.name always starts with "/",
+ // however SstFileMetaData.db_path is the string provided by
+ // the user as an input. Therefore we check if we can
+ // concantenate the two strings directly or if we need to
+ // drop a possible extra "/" at the end of SstFileMetaData.db_path.
+ std::string filename =
+ NormalizePath(sst_metadata.db_path + "/" + sst_metadata.name);
+ all_files.emplace_back(filename, level, cf);
+ } // End of for-loop over sst files
+ } // End of for-loop over levels
+
+ const auto& blob_files = column_metadata.blob_files;
+ for (const auto& blob_metadata : blob_files) {
+ // The BlobMetaData.blob_file_name always starts with "/",
+ // however BlobMetaData.blob_file_path is the string provided by
+ // the user as an input. Therefore we check if we can
+ // concantenate the two strings directly or if we need to
+ // drop a possible extra "/" at the end of BlobMetaData.blob_file_path.
+ std::string filename = NormalizePath(
+ blob_metadata.blob_file_path + "/" + blob_metadata.blob_file_name);
+ // Level for blob files is encoded as -1
+ all_files.emplace_back(filename, -1, cf);
+ } // End of for-loop over blob files
+ } // End of for-loop over column metadata
+
+ // Sort by filename (i.e. first entry in tuple)
+ std::sort(all_files.begin(), all_files.end());
+
+ for (const auto& item : all_files) {
+ const std::string& filename = std::get<0>(item);
+ int level = std::get<1>(item);
+ const std::string& cf = std::get<2>(item);
+ if (level == -1) { // Blob File
+ std::cout << filename << ", column family '" << cf << "'" << std::endl;
+ } else { // SST file
+ std::cout << filename << " : level " << level << ", column family '"
+ << cf << "'" << std::endl;
+ }
+ }
+ } else {
+ for (const auto& column_metadata : metadata) {
+ std::cout << "===== Column Family: " << column_metadata.name
+ << " =====" << std::endl;
+
+ std::cout << "Live SST Files:" << std::endl;
+ // Iterate levels
+ const auto& levels = column_metadata.levels;
+ for (const auto& level_metadata : levels) {
+ std::cout << "---------- level " << level_metadata.level
+ << " ----------" << std::endl;
+ // Iterate SST files
+ const auto& sst_files = level_metadata.files;
+ for (const auto& sst_metadata : sst_files) {
+ // The SstFileMetaData.name always starts with "/",
+ // however SstFileMetaData.db_path is the string provided by
+ // the user as an input. Therefore we check if we can
+ // concantenate the two strings directly or if we need to
+ // drop a possible extra "/" at the end of SstFileMetaData.db_path.
+ std::string filename =
+ NormalizePath(sst_metadata.db_path + "/" + sst_metadata.name);
+ std::cout << filename << std::endl;
+ } // End of for-loop over sst files
+ } // End of for-loop over levels
+
+ std::cout << "Live Blob Files:" << std::endl;
+ const auto& blob_files = column_metadata.blob_files;
+ for (const auto& blob_metadata : blob_files) {
+ // The BlobMetaData.blob_file_name always starts with "/",
+ // however BlobMetaData.blob_file_path is the string provided by
+ // the user as an input. Therefore we check if we can
+ // concantenate the two strings directly or if we need to
+ // drop a possible extra "/" at the end of BlobMetaData.blob_file_path.
+ std::string filename = NormalizePath(
+ blob_metadata.blob_file_path + "/" + blob_metadata.blob_file_name);
+ std::cout << filename << std::endl;
+ } // End of for-loop over blob files
+ } // End of for-loop over column metadata
+ } // End of else ("not sort_by_filename")
+ std::cout << "------------------------------" << std::endl;
+}
+
+void WriteExternalSstFilesCommand::Help(std::string& ret) {
+ ret.append(" ");
+ ret.append(WriteExternalSstFilesCommand::Name());
+ ret.append(" <output_sst_path>");
+ ret.append("\n");
+}
+
+WriteExternalSstFilesCommand::WriteExternalSstFilesCommand(
+ const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags)
+ : LDBCommand(
+ options, flags, false /* is_read_only */,
+ BuildCmdLineOptions({ARG_HEX, ARG_KEY_HEX, ARG_VALUE_HEX, ARG_FROM,
+ ARG_TO, ARG_CREATE_IF_MISSING})) {
+ create_if_missing_ =
+ IsFlagPresent(flags, ARG_CREATE_IF_MISSING) ||
+ ParseBooleanOption(options, ARG_CREATE_IF_MISSING, false);
+ if (params.size() != 1) {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ "output SST file path must be specified");
+ } else {
+ output_sst_path_ = params.at(0);
+ }
+}
+
+void WriteExternalSstFilesCommand::DoCommand() {
+ if (!db_) {
+ assert(GetExecuteState().IsFailed());
+ return;
+ }
+ ColumnFamilyHandle* cfh = GetCfHandle();
+ SstFileWriter sst_file_writer(EnvOptions(), db_->GetOptions(), cfh);
+ Status status = sst_file_writer.Open(output_sst_path_);
+ if (!status.ok()) {
+ exec_state_ = LDBCommandExecuteResult::Failed("failed to open SST file: " +
+ status.ToString());
+ return;
+ }
+
+ int bad_lines = 0;
+ std::string line;
+ std::ifstream ifs_stdin("/dev/stdin");
+ std::istream* istream_p = ifs_stdin.is_open() ? &ifs_stdin : &std::cin;
+ while (getline(*istream_p, line, '\n')) {
+ std::string key;
+ std::string value;
+ if (ParseKeyValue(line, &key, &value, is_key_hex_, is_value_hex_)) {
+ status = sst_file_writer.Put(key, value);
+ if (!status.ok()) {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ "failed to write record to file: " + status.ToString());
+ return;
+ }
+ } else if (0 == line.find("Keys in range:")) {
+ // ignore this line
+ } else if (0 == line.find("Created bg thread 0x")) {
+ // ignore this line
+ } else {
+ bad_lines++;
+ }
+ }
+
+ status = sst_file_writer.Finish();
+ if (!status.ok()) {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ "Failed to finish writing to file: " + status.ToString());
+ return;
+ }
+
+ if (bad_lines > 0) {
+ fprintf(stderr, "Warning: %d bad lines ignored.\n", bad_lines);
+ }
+ exec_state_ = LDBCommandExecuteResult::Succeed(
+ "external SST file written to " + output_sst_path_);
+}
+
+void WriteExternalSstFilesCommand::OverrideBaseOptions() {
+ LDBCommand::OverrideBaseOptions();
+ options_.create_if_missing = create_if_missing_;
+}
+
+const std::string IngestExternalSstFilesCommand::ARG_MOVE_FILES = "move_files";
+const std::string IngestExternalSstFilesCommand::ARG_SNAPSHOT_CONSISTENCY =
+ "snapshot_consistency";
+const std::string IngestExternalSstFilesCommand::ARG_ALLOW_GLOBAL_SEQNO =
+ "allow_global_seqno";
+const std::string IngestExternalSstFilesCommand::ARG_ALLOW_BLOCKING_FLUSH =
+ "allow_blocking_flush";
+const std::string IngestExternalSstFilesCommand::ARG_INGEST_BEHIND =
+ "ingest_behind";
+const std::string IngestExternalSstFilesCommand::ARG_WRITE_GLOBAL_SEQNO =
+ "write_global_seqno";
+
+void IngestExternalSstFilesCommand::Help(std::string& ret) {
+ ret.append(" ");
+ ret.append(IngestExternalSstFilesCommand::Name());
+ ret.append(" <input_sst_path>");
+ ret.append(" [--" + ARG_MOVE_FILES + "] ");
+ ret.append(" [--" + ARG_SNAPSHOT_CONSISTENCY + "] ");
+ ret.append(" [--" + ARG_ALLOW_GLOBAL_SEQNO + "] ");
+ ret.append(" [--" + ARG_ALLOW_BLOCKING_FLUSH + "] ");
+ ret.append(" [--" + ARG_INGEST_BEHIND + "] ");
+ ret.append(" [--" + ARG_WRITE_GLOBAL_SEQNO + "] ");
+ ret.append("\n");
+}
+
+IngestExternalSstFilesCommand::IngestExternalSstFilesCommand(
+ const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags)
+ : LDBCommand(
+ options, flags, false /* is_read_only */,
+ BuildCmdLineOptions({ARG_MOVE_FILES, ARG_SNAPSHOT_CONSISTENCY,
+ ARG_ALLOW_GLOBAL_SEQNO, ARG_CREATE_IF_MISSING,
+ ARG_ALLOW_BLOCKING_FLUSH, ARG_INGEST_BEHIND,
+ ARG_WRITE_GLOBAL_SEQNO})),
+ move_files_(false),
+ snapshot_consistency_(true),
+ allow_global_seqno_(true),
+ allow_blocking_flush_(true),
+ ingest_behind_(false),
+ write_global_seqno_(true) {
+ create_if_missing_ =
+ IsFlagPresent(flags, ARG_CREATE_IF_MISSING) ||
+ ParseBooleanOption(options, ARG_CREATE_IF_MISSING, false);
+ move_files_ = IsFlagPresent(flags, ARG_MOVE_FILES) ||
+ ParseBooleanOption(options, ARG_MOVE_FILES, false);
+ snapshot_consistency_ =
+ IsFlagPresent(flags, ARG_SNAPSHOT_CONSISTENCY) ||
+ ParseBooleanOption(options, ARG_SNAPSHOT_CONSISTENCY, true);
+ allow_global_seqno_ =
+ IsFlagPresent(flags, ARG_ALLOW_GLOBAL_SEQNO) ||
+ ParseBooleanOption(options, ARG_ALLOW_GLOBAL_SEQNO, true);
+ allow_blocking_flush_ =
+ IsFlagPresent(flags, ARG_ALLOW_BLOCKING_FLUSH) ||
+ ParseBooleanOption(options, ARG_ALLOW_BLOCKING_FLUSH, true);
+ ingest_behind_ = IsFlagPresent(flags, ARG_INGEST_BEHIND) ||
+ ParseBooleanOption(options, ARG_INGEST_BEHIND, false);
+ write_global_seqno_ =
+ IsFlagPresent(flags, ARG_WRITE_GLOBAL_SEQNO) ||
+ ParseBooleanOption(options, ARG_WRITE_GLOBAL_SEQNO, true);
+
+ if (allow_global_seqno_) {
+ if (!write_global_seqno_) {
+ fprintf(stderr,
+ "Warning: not writing global_seqno to the ingested SST can\n"
+ "prevent older versions of RocksDB from being able to open it\n");
+ }
+ } else {
+ if (write_global_seqno_) {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ "ldb cannot write global_seqno to the ingested SST when global_seqno "
+ "is not allowed");
+ }
+ }
+
+ if (params.size() != 1) {
+ exec_state_ =
+ LDBCommandExecuteResult::Failed("input SST path must be specified");
+ } else {
+ input_sst_path_ = params.at(0);
+ }
+}
+
+void IngestExternalSstFilesCommand::DoCommand() {
+ if (!db_) {
+ assert(GetExecuteState().IsFailed());
+ return;
+ }
+ if (GetExecuteState().IsFailed()) {
+ return;
+ }
+ ColumnFamilyHandle* cfh = GetCfHandle();
+ IngestExternalFileOptions ifo;
+ ifo.move_files = move_files_;
+ ifo.snapshot_consistency = snapshot_consistency_;
+ ifo.allow_global_seqno = allow_global_seqno_;
+ ifo.allow_blocking_flush = allow_blocking_flush_;
+ ifo.ingest_behind = ingest_behind_;
+ ifo.write_global_seqno = write_global_seqno_;
+ Status status = db_->IngestExternalFile(cfh, {input_sst_path_}, ifo);
+ if (!status.ok()) {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ "failed to ingest external SST: " + status.ToString());
+ } else {
+ exec_state_ =
+ LDBCommandExecuteResult::Succeed("external SST files ingested");
+ }
+}
+
+void IngestExternalSstFilesCommand::OverrideBaseOptions() {
+ LDBCommand::OverrideBaseOptions();
+ options_.create_if_missing = create_if_missing_;
+}
+
+ListFileRangeDeletesCommand::ListFileRangeDeletesCommand(
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags)
+ : LDBCommand(options, flags, true, BuildCmdLineOptions({ARG_MAX_KEYS})) {
+ auto itr = options.find(ARG_MAX_KEYS);
+ if (itr != options.end()) {
+ try {
+#if defined(CYGWIN)
+ max_keys_ = strtol(itr->second.c_str(), 0, 10);
+#else
+ max_keys_ = std::stoi(itr->second);
+#endif
+ } catch (const std::invalid_argument&) {
+ exec_state_ = LDBCommandExecuteResult::Failed(ARG_MAX_KEYS +
+ " has an invalid value");
+ } catch (const std::out_of_range&) {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ ARG_MAX_KEYS + " has a value out-of-range");
+ }
+ }
+}
+
+void ListFileRangeDeletesCommand::Help(std::string& ret) {
+ ret.append(" ");
+ ret.append(ListFileRangeDeletesCommand::Name());
+ ret.append(" [--" + ARG_MAX_KEYS + "=<N>]");
+ ret.append(" : print tombstones in SST files.\n");
+}
+
+void ListFileRangeDeletesCommand::DoCommand() {
+ if (!db_) {
+ assert(GetExecuteState().IsFailed());
+ return;
+ }
+
+ DBImpl* db_impl = static_cast_with_check<DBImpl>(db_->GetRootDB());
+
+ std::string out_str;
+
+ Status st =
+ db_impl->TablesRangeTombstoneSummary(GetCfHandle(), max_keys_, &out_str);
+ if (st.ok()) {
+ TEST_SYNC_POINT_CALLBACK(
+ "ListFileRangeDeletesCommand::DoCommand:BeforePrint", &out_str);
+ fprintf(stdout, "%s\n", out_str.c_str());
+ }
+}
+
+void UnsafeRemoveSstFileCommand::Help(std::string& ret) {
+ ret.append(" ");
+ ret.append(UnsafeRemoveSstFileCommand::Name());
+ ret.append(" <SST file number>");
+ ret.append(" ");
+ ret.append(" MUST NOT be used on a live DB.");
+ ret.append("\n");
+}
+
+UnsafeRemoveSstFileCommand::UnsafeRemoveSstFileCommand(
+ const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags)
+ : LDBCommand(options, flags, false /* is_read_only */,
+ BuildCmdLineOptions({})) {
+ if (params.size() != 1) {
+ exec_state_ =
+ LDBCommandExecuteResult::Failed("SST file number must be specified");
+ } else {
+ char* endptr = nullptr;
+ sst_file_number_ = strtoull(params.at(0).c_str(), &endptr, 10 /* base */);
+ if (endptr == nullptr || *endptr != '\0') {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ "Failed to parse SST file number " + params.at(0));
+ }
+ }
+}
+
+void UnsafeRemoveSstFileCommand::DoCommand() {
+ PrepareOptions();
+
+ OfflineManifestWriter w(options_, db_path_);
+ if (column_families_.empty()) {
+ column_families_.emplace_back(kDefaultColumnFamilyName, options_);
+ }
+ Status s = w.Recover(column_families_);
+
+ ColumnFamilyData* cfd = nullptr;
+ int level = -1;
+ if (s.ok()) {
+ FileMetaData* metadata = nullptr;
+ s = w.Versions().GetMetadataForFile(sst_file_number_, &level, &metadata,
+ &cfd);
+ }
+
+ if (s.ok()) {
+ VersionEdit edit;
+ edit.SetColumnFamily(cfd->GetID());
+ edit.DeleteFile(level, sst_file_number_);
+ std::unique_ptr<FSDirectory> db_dir;
+ s = options_.env->GetFileSystem()->NewDirectory(db_path_, IOOptions(),
+ &db_dir, nullptr);
+ if (s.ok()) {
+ s = w.LogAndApply(cfd, &edit, db_dir.get());
+ }
+ }
+
+ if (!s.ok()) {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ "failed to unsafely remove SST file: " + s.ToString());
+ } else {
+ exec_state_ = LDBCommandExecuteResult::Succeed("unsafely removed SST file");
+ }
+}
+
+const std::string UpdateManifestCommand::ARG_VERBOSE = "verbose";
+const std::string UpdateManifestCommand::ARG_UPDATE_TEMPERATURES =
+ "update_temperatures";
+
+void UpdateManifestCommand::Help(std::string& ret) {
+ ret.append(" ");
+ ret.append(UpdateManifestCommand::Name());
+ ret.append(" [--update_temperatures]");
+ ret.append(" ");
+ ret.append(" MUST NOT be used on a live DB.");
+ ret.append("\n");
+}
+
+UpdateManifestCommand::UpdateManifestCommand(
+ const std::vector<std::string>& /*params*/,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags)
+ : LDBCommand(options, flags, false /* is_read_only */,
+ BuildCmdLineOptions({ARG_VERBOSE, ARG_UPDATE_TEMPERATURES})) {
+ verbose_ = IsFlagPresent(flags, ARG_VERBOSE) ||
+ ParseBooleanOption(options, ARG_VERBOSE, false);
+ update_temperatures_ =
+ IsFlagPresent(flags, ARG_UPDATE_TEMPERATURES) ||
+ ParseBooleanOption(options, ARG_UPDATE_TEMPERATURES, false);
+
+ if (!update_temperatures_) {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ "No action like --update_temperatures specified for update_manifest");
+ }
+}
+
+void UpdateManifestCommand::DoCommand() {
+ PrepareOptions();
+
+ auto level = verbose_ ? InfoLogLevel::INFO_LEVEL : InfoLogLevel::WARN_LEVEL;
+ options_.info_log.reset(new StderrLogger(level));
+
+ experimental::UpdateManifestForFilesStateOptions opts;
+ opts.update_temperatures = update_temperatures_;
+ if (column_families_.empty()) {
+ column_families_.emplace_back(kDefaultColumnFamilyName, options_);
+ }
+ Status s = experimental::UpdateManifestForFilesState(options_, db_path_,
+ column_families_);
+
+ if (!s.ok()) {
+ exec_state_ = LDBCommandExecuteResult::Failed(
+ "failed to update manifest: " + s.ToString());
+ } else {
+ exec_state_ =
+ LDBCommandExecuteResult::Succeed("Manifest updates successful");
+ }
+}
+
+} // namespace ROCKSDB_NAMESPACE
+#endif // ROCKSDB_LITE
diff --git a/src/rocksdb/tools/ldb_cmd_impl.h b/src/rocksdb/tools/ldb_cmd_impl.h
new file mode 100644
index 000000000..97de981b1
--- /dev/null
+++ b/src/rocksdb/tools/ldb_cmd_impl.h
@@ -0,0 +1,744 @@
+// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+// This source code is licensed under both the GPLv2 (found in the
+// COPYING file in the root directory) and Apache 2.0 License
+// (found in the LICENSE.Apache file in the root directory).
+
+#pragma once
+
+#include <map>
+#include <string>
+#include <utility>
+#include <vector>
+
+#include "rocksdb/utilities/ldb_cmd.h"
+
+namespace ROCKSDB_NAMESPACE {
+
+class CompactorCommand : public LDBCommand {
+ public:
+ static std::string Name() { return "compact"; }
+
+ CompactorCommand(const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags);
+
+ static void Help(std::string& ret);
+
+ void DoCommand() override;
+
+ private:
+ bool null_from_;
+ std::string from_;
+ bool null_to_;
+ std::string to_;
+};
+
+class DBFileDumperCommand : public LDBCommand {
+ public:
+ static std::string Name() { return "dump_live_files"; }
+
+ DBFileDumperCommand(const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags);
+
+ static void Help(std::string& ret);
+
+ void DoCommand() override;
+
+ private:
+ bool decode_blob_index_;
+ bool dump_uncompressed_blobs_;
+};
+
+class DBLiveFilesMetadataDumperCommand : public LDBCommand {
+ public:
+ static std::string Name() { return "list_live_files_metadata"; }
+
+ DBLiveFilesMetadataDumperCommand(
+ const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags);
+
+ static void Help(std::string& ret);
+
+ void DoCommand() override;
+
+ private:
+ bool sort_by_filename_;
+
+ static const std::string ARG_SORT_BY_FILENAME;
+};
+
+class DBDumperCommand : public LDBCommand {
+ public:
+ static std::string Name() { return "dump"; }
+
+ DBDumperCommand(const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags);
+
+ static void Help(std::string& ret);
+
+ void DoCommand() override;
+
+ private:
+ /**
+ * Extract file name from the full path. We handle both the forward slash (/)
+ * and backslash (\) to make sure that different OS-s are supported.
+ */
+ static std::string GetFileNameFromPath(const std::string& s) {
+ std::size_t n = s.find_last_of("/\\");
+
+ if (std::string::npos == n) {
+ return s;
+ } else {
+ return s.substr(n + 1);
+ }
+ }
+
+ void DoDumpCommand();
+
+ bool null_from_;
+ std::string from_;
+ bool null_to_;
+ std::string to_;
+ int max_keys_;
+ std::string delim_;
+ bool count_only_;
+ bool count_delim_;
+ bool print_stats_;
+ std::string path_;
+ bool decode_blob_index_;
+ bool dump_uncompressed_blobs_;
+
+ static const std::string ARG_COUNT_ONLY;
+ static const std::string ARG_COUNT_DELIM;
+ static const std::string ARG_STATS;
+ static const std::string ARG_TTL_BUCKET;
+};
+
+class InternalDumpCommand : public LDBCommand {
+ public:
+ static std::string Name() { return "idump"; }
+
+ InternalDumpCommand(const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags);
+
+ static void Help(std::string& ret);
+
+ void DoCommand() override;
+
+ private:
+ bool has_from_;
+ std::string from_;
+ bool has_to_;
+ std::string to_;
+ int max_keys_;
+ std::string delim_;
+ bool count_only_;
+ bool count_delim_;
+ bool print_stats_;
+ bool is_input_key_hex_;
+ bool decode_blob_index_;
+
+ static const std::string ARG_DELIM;
+ static const std::string ARG_COUNT_ONLY;
+ static const std::string ARG_COUNT_DELIM;
+ static const std::string ARG_STATS;
+ static const std::string ARG_INPUT_KEY_HEX;
+};
+
+class DBLoaderCommand : public LDBCommand {
+ public:
+ static std::string Name() { return "load"; }
+
+ DBLoaderCommand(std::string& db_name, std::vector<std::string>& args);
+
+ DBLoaderCommand(const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags);
+
+ static void Help(std::string& ret);
+
+ void DoCommand() override;
+
+ void OverrideBaseOptions() override;
+
+ private:
+ bool disable_wal_;
+ bool bulk_load_;
+ bool compact_;
+
+ static const std::string ARG_DISABLE_WAL;
+ static const std::string ARG_BULK_LOAD;
+ static const std::string ARG_COMPACT;
+};
+
+class ManifestDumpCommand : public LDBCommand {
+ public:
+ static std::string Name() { return "manifest_dump"; }
+
+ ManifestDumpCommand(const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags);
+
+ static void Help(std::string& ret);
+
+ void DoCommand() override;
+
+ bool NoDBOpen() override { return true; }
+
+ private:
+ bool verbose_;
+ bool json_;
+ std::string path_;
+
+ static const std::string ARG_VERBOSE;
+ static const std::string ARG_JSON;
+ static const std::string ARG_PATH;
+};
+
+class UpdateManifestCommand : public LDBCommand {
+ public:
+ static std::string Name() { return "update_manifest"; }
+
+ UpdateManifestCommand(const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags);
+
+ static void Help(std::string& ret);
+ virtual void DoCommand() override;
+
+ virtual bool NoDBOpen() override { return true; }
+
+ private:
+ bool verbose_;
+ bool update_temperatures_;
+ // TODO future: checksum_func for populating checksums
+
+ static const std::string ARG_VERBOSE;
+ static const std::string ARG_UPDATE_TEMPERATURES;
+};
+
+class FileChecksumDumpCommand : public LDBCommand {
+ public:
+ static std::string Name() { return "file_checksum_dump"; }
+
+ FileChecksumDumpCommand(const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags);
+
+ static void Help(std::string& ret);
+ void DoCommand() override;
+
+ bool NoDBOpen() override { return true; }
+
+ private:
+ std::string path_;
+ bool is_checksum_hex_;
+
+ static const std::string ARG_PATH;
+};
+
+class GetPropertyCommand : public LDBCommand {
+ public:
+ static std::string Name() { return "get_property"; }
+
+ GetPropertyCommand(const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags);
+
+ static void Help(std::string& ret);
+ void DoCommand() override;
+
+ private:
+ std::string property_;
+};
+
+class ListColumnFamiliesCommand : public LDBCommand {
+ public:
+ static std::string Name() { return "list_column_families"; }
+
+ ListColumnFamiliesCommand(const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags);
+
+ static void Help(std::string& ret);
+
+ void DoCommand() override;
+
+ bool NoDBOpen() override { return true; }
+};
+
+class CreateColumnFamilyCommand : public LDBCommand {
+ public:
+ static std::string Name() { return "create_column_family"; }
+
+ CreateColumnFamilyCommand(const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags);
+
+ static void Help(std::string& ret);
+
+ void DoCommand() override;
+
+ bool NoDBOpen() override { return false; }
+
+ private:
+ std::string new_cf_name_;
+};
+
+class DropColumnFamilyCommand : public LDBCommand {
+ public:
+ static std::string Name() { return "drop_column_family"; }
+
+ DropColumnFamilyCommand(const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags);
+
+ static void Help(std::string& ret);
+
+ void DoCommand() override;
+
+ bool NoDBOpen() override { return false; }
+
+ private:
+ std::string cf_name_to_drop_;
+};
+
+class ReduceDBLevelsCommand : public LDBCommand {
+ public:
+ static std::string Name() { return "reduce_levels"; }
+
+ ReduceDBLevelsCommand(const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags);
+
+ void OverrideBaseCFOptions(ColumnFamilyOptions* cf_opts) override;
+
+ void DoCommand() override;
+
+ bool NoDBOpen() override { return true; }
+
+ static void Help(std::string& msg);
+
+ static std::vector<std::string> PrepareArgs(const std::string& db_path,
+ int new_levels,
+ bool print_old_level = false);
+
+ private:
+ int old_levels_;
+ int new_levels_;
+ bool print_old_levels_;
+
+ static const std::string ARG_NEW_LEVELS;
+ static const std::string ARG_PRINT_OLD_LEVELS;
+
+ Status GetOldNumOfLevels(Options& opt, int* levels);
+};
+
+class ChangeCompactionStyleCommand : public LDBCommand {
+ public:
+ static std::string Name() { return "change_compaction_style"; }
+
+ ChangeCompactionStyleCommand(
+ const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags);
+
+ void OverrideBaseCFOptions(ColumnFamilyOptions* cf_opts) override;
+
+ void DoCommand() override;
+
+ static void Help(std::string& msg);
+
+ private:
+ int old_compaction_style_;
+ int new_compaction_style_;
+
+ static const std::string ARG_OLD_COMPACTION_STYLE;
+ static const std::string ARG_NEW_COMPACTION_STYLE;
+};
+
+class WALDumperCommand : public LDBCommand {
+ public:
+ static std::string Name() { return "dump_wal"; }
+
+ WALDumperCommand(const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags);
+
+ bool NoDBOpen() override { return true; }
+
+ static void Help(std::string& ret);
+
+ void DoCommand() override;
+
+ private:
+ bool print_header_;
+ std::string wal_file_;
+ bool print_values_;
+ bool is_write_committed_; // default will be set to true
+
+ static const std::string ARG_WAL_FILE;
+ static const std::string ARG_WRITE_COMMITTED;
+ static const std::string ARG_PRINT_HEADER;
+ static const std::string ARG_PRINT_VALUE;
+};
+
+class GetCommand : public LDBCommand {
+ public:
+ static std::string Name() { return "get"; }
+
+ GetCommand(const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags);
+
+ void DoCommand() override;
+
+ static void Help(std::string& ret);
+
+ private:
+ std::string key_;
+};
+
+class ApproxSizeCommand : public LDBCommand {
+ public:
+ static std::string Name() { return "approxsize"; }
+
+ ApproxSizeCommand(const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags);
+
+ void DoCommand() override;
+
+ static void Help(std::string& ret);
+
+ private:
+ std::string start_key_;
+ std::string end_key_;
+};
+
+class BatchPutCommand : public LDBCommand {
+ public:
+ static std::string Name() { return "batchput"; }
+
+ BatchPutCommand(const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags);
+
+ void DoCommand() override;
+
+ static void Help(std::string& ret);
+
+ void OverrideBaseOptions() override;
+
+ private:
+ /**
+ * The key-values to be inserted.
+ */
+ std::vector<std::pair<std::string, std::string>> key_values_;
+};
+
+class ScanCommand : public LDBCommand {
+ public:
+ static std::string Name() { return "scan"; }
+
+ ScanCommand(const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags);
+
+ void DoCommand() override;
+
+ static void Help(std::string& ret);
+
+ private:
+ std::string start_key_;
+ std::string end_key_;
+ bool start_key_specified_;
+ bool end_key_specified_;
+ int max_keys_scanned_;
+ bool no_value_;
+};
+
+class DeleteCommand : public LDBCommand {
+ public:
+ static std::string Name() { return "delete"; }
+
+ DeleteCommand(const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags);
+
+ void DoCommand() override;
+
+ static void Help(std::string& ret);
+
+ private:
+ std::string key_;
+};
+
+class SingleDeleteCommand : public LDBCommand {
+ public:
+ static std::string Name() { return "singledelete"; }
+
+ SingleDeleteCommand(const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags);
+
+ void DoCommand() override;
+
+ static void Help(std::string& ret);
+
+ private:
+ std::string key_;
+};
+
+class DeleteRangeCommand : public LDBCommand {
+ public:
+ static std::string Name() { return "deleterange"; }
+
+ DeleteRangeCommand(const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags);
+
+ void DoCommand() override;
+
+ static void Help(std::string& ret);
+
+ private:
+ std::string begin_key_;
+ std::string end_key_;
+};
+
+class PutCommand : public LDBCommand {
+ public:
+ static std::string Name() { return "put"; }
+
+ PutCommand(const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags);
+
+ void DoCommand() override;
+
+ static void Help(std::string& ret);
+
+ void OverrideBaseOptions() override;
+
+ private:
+ std::string key_;
+ std::string value_;
+};
+
+/**
+ * Command that starts up a REPL shell that allows
+ * get/put/delete.
+ */
+class DBQuerierCommand : public LDBCommand {
+ public:
+ static std::string Name() { return "query"; }
+
+ DBQuerierCommand(const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags);
+
+ static void Help(std::string& ret);
+
+ void DoCommand() override;
+
+ private:
+ static const char* HELP_CMD;
+ static const char* GET_CMD;
+ static const char* PUT_CMD;
+ static const char* DELETE_CMD;
+};
+
+class CheckConsistencyCommand : public LDBCommand {
+ public:
+ static std::string Name() { return "checkconsistency"; }
+
+ CheckConsistencyCommand(const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags);
+
+ void DoCommand() override;
+
+ bool NoDBOpen() override { return true; }
+
+ static void Help(std::string& ret);
+};
+
+class CheckPointCommand : public LDBCommand {
+ public:
+ static std::string Name() { return "checkpoint"; }
+
+ CheckPointCommand(const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags);
+
+ void DoCommand() override;
+
+ static void Help(std::string& ret);
+
+ std::string checkpoint_dir_;
+
+ private:
+ static const std::string ARG_CHECKPOINT_DIR;
+};
+
+class RepairCommand : public LDBCommand {
+ public:
+ static std::string Name() { return "repair"; }
+
+ RepairCommand(const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags);
+
+ void DoCommand() override;
+
+ bool NoDBOpen() override { return true; }
+
+ void OverrideBaseOptions() override;
+
+ static void Help(std::string& ret);
+
+ protected:
+ bool verbose_;
+
+ private:
+ static const std::string ARG_VERBOSE;
+};
+
+class BackupEngineCommand : public LDBCommand {
+ public:
+ BackupEngineCommand(const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags);
+
+ protected:
+ static void Help(const std::string& name, std::string& ret);
+ std::string backup_env_uri_;
+ std::string backup_fs_uri_;
+ std::string backup_dir_;
+ int num_threads_;
+ std::unique_ptr<Logger> logger_;
+ std::shared_ptr<Env> backup_env_guard_;
+
+ private:
+ static const std::string ARG_BACKUP_DIR;
+ static const std::string ARG_BACKUP_ENV_URI;
+ static const std::string ARG_BACKUP_FS_URI;
+ static const std::string ARG_NUM_THREADS;
+ static const std::string ARG_STDERR_LOG_LEVEL;
+};
+
+class BackupCommand : public BackupEngineCommand {
+ public:
+ static std::string Name() { return "backup"; }
+ BackupCommand(const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags);
+ void DoCommand() override;
+ static void Help(std::string& ret);
+};
+
+class RestoreCommand : public BackupEngineCommand {
+ public:
+ static std::string Name() { return "restore"; }
+ RestoreCommand(const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags);
+ void DoCommand() override;
+ bool NoDBOpen() override { return true; }
+ static void Help(std::string& ret);
+};
+
+class WriteExternalSstFilesCommand : public LDBCommand {
+ public:
+ static std::string Name() { return "write_extern_sst"; }
+ WriteExternalSstFilesCommand(
+ const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags);
+
+ void DoCommand() override;
+
+ bool NoDBOpen() override { return false; }
+
+ void OverrideBaseOptions() override;
+
+ static void Help(std::string& ret);
+
+ private:
+ std::string output_sst_path_;
+};
+
+class IngestExternalSstFilesCommand : public LDBCommand {
+ public:
+ static std::string Name() { return "ingest_extern_sst"; }
+ IngestExternalSstFilesCommand(
+ const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags);
+
+ void DoCommand() override;
+
+ bool NoDBOpen() override { return false; }
+
+ void OverrideBaseOptions() override;
+
+ static void Help(std::string& ret);
+
+ private:
+ std::string input_sst_path_;
+ bool move_files_;
+ bool snapshot_consistency_;
+ bool allow_global_seqno_;
+ bool allow_blocking_flush_;
+ bool ingest_behind_;
+ bool write_global_seqno_;
+
+ static const std::string ARG_MOVE_FILES;
+ static const std::string ARG_SNAPSHOT_CONSISTENCY;
+ static const std::string ARG_ALLOW_GLOBAL_SEQNO;
+ static const std::string ARG_ALLOW_BLOCKING_FLUSH;
+ static const std::string ARG_INGEST_BEHIND;
+ static const std::string ARG_WRITE_GLOBAL_SEQNO;
+};
+
+// Command that prints out range delete tombstones in SST files.
+class ListFileRangeDeletesCommand : public LDBCommand {
+ public:
+ static std::string Name() { return "list_file_range_deletes"; }
+
+ ListFileRangeDeletesCommand(const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags);
+
+ void DoCommand() override;
+
+ static void Help(std::string& ret);
+
+ private:
+ int max_keys_ = 1000;
+};
+
+// Command that removes the SST file forcibly from the manifest.
+class UnsafeRemoveSstFileCommand : public LDBCommand {
+ public:
+ static std::string Name() { return "unsafe_remove_sst_file"; }
+
+ UnsafeRemoveSstFileCommand(const std::vector<std::string>& params,
+ const std::map<std::string, std::string>& options,
+ const std::vector<std::string>& flags);
+
+ static void Help(std::string& ret);
+
+ void DoCommand() override;
+
+ bool NoDBOpen() override { return true; }
+
+ private:
+ uint64_t sst_file_number_;
+};
+
+} // namespace ROCKSDB_NAMESPACE
diff --git a/src/rocksdb/tools/ldb_cmd_test.cc b/src/rocksdb/tools/ldb_cmd_test.cc
new file mode 100644
index 000000000..5d83a6cd9
--- /dev/null
+++ b/src/rocksdb/tools/ldb_cmd_test.cc
@@ -0,0 +1,1226 @@
+// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+// This source code is licensed under both the GPLv2 (found in the
+// COPYING file in the root directory) and Apache 2.0 License
+// (found in the LICENSE.Apache file in the root directory).
+//
+#ifndef ROCKSDB_LITE
+#include "rocksdb/utilities/ldb_cmd.h"
+
+#include <cinttypes>
+
+#include "db/db_test_util.h"
+#include "db/version_edit.h"
+#include "db/version_set.h"
+#include "env/composite_env_wrapper.h"
+#include "file/filename.h"
+#include "port/stack_trace.h"
+#include "rocksdb/advanced_options.h"
+#include "rocksdb/convenience.h"
+#include "rocksdb/db.h"
+#include "rocksdb/file_checksum.h"
+#include "rocksdb/file_system.h"
+#include "rocksdb/utilities/options_util.h"
+#include "test_util/sync_point.h"
+#include "test_util/testharness.h"
+#include "test_util/testutil.h"
+#include "util/file_checksum_helper.h"
+#include "util/random.h"
+
+using std::map;
+using std::string;
+using std::vector;
+
+namespace ROCKSDB_NAMESPACE {
+
+class LdbCmdTest : public testing::Test {
+ public:
+ LdbCmdTest() : testing::Test() {}
+
+ Env* TryLoadCustomOrDefaultEnv() {
+ Env* env = Env::Default();
+ EXPECT_OK(test::CreateEnvFromSystem(ConfigOptions(), &env, &env_guard_));
+ return env;
+ }
+
+ private:
+ std::shared_ptr<Env> env_guard_;
+};
+
+TEST_F(LdbCmdTest, HelpAndVersion) {
+ Options o;
+ o.env = TryLoadCustomOrDefaultEnv();
+ LDBOptions lo;
+ static const char* help[] = {"./ldb", "--help"};
+ ASSERT_EQ(0, LDBCommandRunner::RunCommand(2, help, o, lo, nullptr));
+ static const char* version[] = {"./ldb", "--version"};
+ ASSERT_EQ(0, LDBCommandRunner::RunCommand(2, version, o, lo, nullptr));
+ static const char* bad[] = {"./ldb", "--not_an_option"};
+ ASSERT_NE(0, LDBCommandRunner::RunCommand(2, bad, o, lo, nullptr));
+}
+
+TEST_F(LdbCmdTest, HexToString) {
+ // map input to expected outputs.
+ // odd number of "hex" half bytes doesn't make sense
+ map<string, vector<int>> inputMap = {
+ {"0x07", {7}}, {"0x5050", {80, 80}}, {"0xFF", {-1}},
+ {"0x1234", {18, 52}}, {"0xaaAbAC", {-86, -85, -84}}, {"0x1203", {18, 3}},
+ };
+
+ for (const auto& inPair : inputMap) {
+ auto actual = ROCKSDB_NAMESPACE::LDBCommand::HexToString(inPair.first);
+ auto expected = inPair.second;
+ for (unsigned int i = 0; i < actual.length(); i++) {
+ EXPECT_EQ(expected[i], static_cast<int>((signed char)actual[i]));
+ }
+ auto reverse = ROCKSDB_NAMESPACE::LDBCommand::StringToHex(actual);
+ EXPECT_STRCASEEQ(inPair.first.c_str(), reverse.c_str());
+ }
+}
+
+TEST_F(LdbCmdTest, HexToStringBadInputs) {
+ const vector<string> badInputs = {
+ "0xZZ", "123", "0xx5", "0x111G", "0x123", "Ox12", "0xT", "0x1Q1",
+ };
+ for (const auto& badInput : badInputs) {
+ try {
+ ROCKSDB_NAMESPACE::LDBCommand::HexToString(badInput);
+ std::cerr << "Should fail on bad hex value: " << badInput << "\n";
+ FAIL();
+ } catch (...) {
+ }
+ }
+}
+
+TEST_F(LdbCmdTest, MemEnv) {
+ Env* base_env = TryLoadCustomOrDefaultEnv();
+ std::unique_ptr<Env> env(NewMemEnv(base_env));
+ Options opts;
+ opts.env = env.get();
+ opts.create_if_missing = true;
+
+ DB* db = nullptr;
+ std::string dbname = test::PerThreadDBPath(env.get(), "ldb_cmd_test");
+ ASSERT_OK(DB::Open(opts, dbname, &db));
+
+ WriteOptions wopts;
+ for (int i = 0; i < 100; i++) {
+ char buf[16];
+ snprintf(buf, sizeof(buf), "%08d", i);
+ ASSERT_OK(db->Put(wopts, buf, buf));
+ }
+ FlushOptions fopts;
+ fopts.wait = true;
+ ASSERT_OK(db->Flush(fopts));
+
+ delete db;
+
+ char arg1[] = "./ldb";
+ char arg2[1024];
+ snprintf(arg2, sizeof(arg2), "--db=%s", dbname.c_str());
+ char arg3[] = "dump_live_files";
+ char* argv[] = {arg1, arg2, arg3};
+
+ ASSERT_EQ(0,
+ LDBCommandRunner::RunCommand(3, argv, opts, LDBOptions(), nullptr));
+}
+
+class FileChecksumTestHelper {
+ private:
+ Options options_;
+ DB* db_;
+ std::string dbname_;
+
+ Status VerifyChecksum(LiveFileMetaData& file_meta) {
+ std::string cur_checksum;
+ std::string checksum_func_name;
+
+ Status s;
+ EnvOptions soptions;
+ std::unique_ptr<SequentialFile> file_reader;
+ std::string file_path = dbname_ + "/" + file_meta.name;
+ s = options_.env->NewSequentialFile(file_path, &file_reader, soptions);
+ if (!s.ok()) {
+ return s;
+ }
+ std::unique_ptr<char[]> scratch(new char[2048]);
+ Slice result;
+ FileChecksumGenFactory* file_checksum_gen_factory =
+ options_.file_checksum_gen_factory.get();
+ if (file_checksum_gen_factory == nullptr) {
+ cur_checksum = kUnknownFileChecksum;
+ checksum_func_name = kUnknownFileChecksumFuncName;
+ } else {
+ FileChecksumGenContext gen_context;
+ gen_context.file_name = file_meta.name;
+ std::unique_ptr<FileChecksumGenerator> file_checksum_gen =
+ file_checksum_gen_factory->CreateFileChecksumGenerator(gen_context);
+ checksum_func_name = file_checksum_gen->Name();
+ s = file_reader->Read(2048, &result, scratch.get());
+ if (!s.ok()) {
+ return s;
+ }
+ while (result.size() != 0) {
+ file_checksum_gen->Update(scratch.get(), result.size());
+ s = file_reader->Read(2048, &result, scratch.get());
+ if (!s.ok()) {
+ return s;
+ }
+ }
+ file_checksum_gen->Finalize();
+ cur_checksum = file_checksum_gen->GetChecksum();
+ }
+
+ std::string stored_checksum = file_meta.file_checksum;
+ std::string stored_checksum_func_name = file_meta.file_checksum_func_name;
+ if ((cur_checksum != stored_checksum) ||
+ (checksum_func_name != stored_checksum_func_name)) {
+ return Status::Corruption(
+ "Checksum does not match! The file: " + file_meta.name +
+ ", checksum name: " + stored_checksum_func_name + " and checksum " +
+ stored_checksum + ". However, expected checksum name: " +
+ checksum_func_name + " and checksum " + cur_checksum);
+ }
+ return Status::OK();
+ }
+
+ public:
+ FileChecksumTestHelper(Options& options, DB* db, std::string db_name)
+ : options_(options), db_(db), dbname_(db_name) {}
+ ~FileChecksumTestHelper() {}
+
+ // Verify the checksum information in Manifest.
+ Status VerifyChecksumInManifest(
+ const std::vector<LiveFileMetaData>& live_files) {
+ // Step 1: verify if the dbname_ is correct
+ if (dbname_.back() != '/') {
+ dbname_.append("/");
+ }
+
+ // Step 2, get the the checksum information by recovering the VersionSet
+ // from Manifest.
+ std::unique_ptr<FileChecksumList> checksum_list(NewFileChecksumList());
+ EnvOptions sopt;
+ std::shared_ptr<Cache> tc(NewLRUCache(options_.max_open_files - 10,
+ options_.table_cache_numshardbits));
+ options_.db_paths.emplace_back(dbname_, 0);
+ options_.num_levels = 64;
+ WriteController wc(options_.delayed_write_rate);
+ WriteBufferManager wb(options_.db_write_buffer_size);
+ ImmutableDBOptions immutable_db_options(options_);
+ VersionSet versions(dbname_, &immutable_db_options, sopt, tc.get(), &wb,
+ &wc, nullptr, nullptr, "", "");
+ std::vector<std::string> cf_name_list;
+ Status s;
+ s = versions.ListColumnFamilies(&cf_name_list, dbname_,
+ immutable_db_options.fs.get());
+ if (s.ok()) {
+ std::vector<ColumnFamilyDescriptor> cf_list;
+ for (const auto& name : cf_name_list) {
+ fprintf(stdout, "cf_name: %s", name.c_str());
+ cf_list.emplace_back(name, ColumnFamilyOptions(options_));
+ }
+ s = versions.Recover(cf_list, true);
+ }
+ if (s.ok()) {
+ s = versions.GetLiveFilesChecksumInfo(checksum_list.get());
+ }
+ if (!s.ok()) {
+ return s;
+ }
+
+ // Step 3 verify the checksum
+ if (live_files.size() != checksum_list->size()) {
+ return Status::Corruption("The number of files does not match!");
+ }
+ for (size_t i = 0; i < live_files.size(); i++) {
+ std::string stored_checksum = "";
+ std::string stored_func_name = "";
+ s = checksum_list->SearchOneFileChecksum(
+ live_files[i].file_number, &stored_checksum, &stored_func_name);
+ if (s.IsNotFound()) {
+ return s;
+ }
+ if (live_files[i].file_checksum != stored_checksum ||
+ live_files[i].file_checksum_func_name != stored_func_name) {
+ return Status::Corruption(
+ "Checksum does not match! The file: " +
+ std::to_string(live_files[i].file_number) +
+ ". In Manifest, checksum name: " + stored_func_name +
+ " and checksum " + stored_checksum +
+ ". However, expected checksum name: " +
+ live_files[i].file_checksum_func_name + " and checksum " +
+ live_files[i].file_checksum);
+ }
+ }
+ return Status::OK();
+ }
+
+ // Verify the checksum of each file by recalculting the checksum and
+ // comparing it with the one being generated when a SST file is created.
+ Status VerifyEachFileChecksum() {
+ assert(db_ != nullptr);
+ EXPECT_OK(db_->DisableFileDeletions());
+ std::vector<LiveFileMetaData> live_files;
+ db_->GetLiveFilesMetaData(&live_files);
+ Status cs;
+ for (auto a_file : live_files) {
+ cs = VerifyChecksum(a_file);
+ if (!cs.ok()) {
+ break;
+ }
+ }
+ EXPECT_OK(db_->EnableFileDeletions());
+ return cs;
+ }
+};
+
+TEST_F(LdbCmdTest, DumpFileChecksumNoChecksum) {
+ Env* base_env = TryLoadCustomOrDefaultEnv();
+ std::unique_ptr<Env> env(NewMemEnv(base_env));
+ Options opts;
+ opts.env = env.get();
+ opts.create_if_missing = true;
+
+ DB* db = nullptr;
+ std::string dbname = test::PerThreadDBPath(env.get(), "ldb_cmd_test");
+ ASSERT_OK(DB::Open(opts, dbname, &db));
+
+ WriteOptions wopts;
+ FlushOptions fopts;
+ fopts.wait = true;
+ Random rnd(test::RandomSeed());
+ for (int i = 0; i < 200; i++) {
+ char buf[16];
+ snprintf(buf, sizeof(buf), "%08d", i);
+ std::string v = rnd.RandomString(100);
+ ASSERT_OK(db->Put(wopts, buf, v));
+ }
+ ASSERT_OK(db->Flush(fopts));
+ for (int i = 100; i < 300; i++) {
+ char buf[16];
+ snprintf(buf, sizeof(buf), "%08d", i);
+ std::string v = rnd.RandomString(100);
+ ASSERT_OK(db->Put(wopts, buf, v));
+ }
+ ASSERT_OK(db->Flush(fopts));
+ for (int i = 200; i < 400; i++) {
+ char buf[16];
+ snprintf(buf, sizeof(buf), "%08d", i);
+ std::string v = rnd.RandomString(100);
+ ASSERT_OK(db->Put(wopts, buf, v));
+ }
+ ASSERT_OK(db->Flush(fopts));
+ for (int i = 300; i < 400; i++) {
+ char buf[16];
+ snprintf(buf, sizeof(buf), "%08d", i);
+ std::string v = rnd.RandomString(100);
+ ASSERT_OK(db->Put(wopts, buf, v));
+ }
+ ASSERT_OK(db->Flush(fopts));
+ ASSERT_OK(db->Close());
+ delete db;
+
+ char arg1[] = "./ldb";
+ char arg2[1024];
+ snprintf(arg2, sizeof(arg2), "--db=%s", dbname.c_str());
+ char arg3[] = "file_checksum_dump";
+ char arg4[] = "--hex";
+ char* argv[] = {arg1, arg2, arg3, arg4};
+
+ ASSERT_EQ(0,
+ LDBCommandRunner::RunCommand(4, argv, opts, LDBOptions(), nullptr));
+
+ ASSERT_OK(DB::Open(opts, dbname, &db));
+
+ // Verify each sst file checksum value and checksum name
+ FileChecksumTestHelper fct_helper(opts, db, dbname);
+ ASSERT_OK(fct_helper.VerifyEachFileChecksum());
+
+ // Manually trigger compaction
+ char b_buf[16];
+ snprintf(b_buf, sizeof(b_buf), "%08d", 0);
+ char e_buf[16];
+ snprintf(e_buf, sizeof(e_buf), "%08d", 399);
+ Slice begin(b_buf);
+ Slice end(e_buf);
+ CompactRangeOptions options;
+ ASSERT_OK(db->CompactRange(options, &begin, &end));
+ // Verify each sst file checksum after compaction
+ FileChecksumTestHelper fct_helper_ac(opts, db, dbname);
+ ASSERT_OK(fct_helper_ac.VerifyEachFileChecksum());
+
+ ASSERT_OK(db->Close());
+ delete db;
+
+ ASSERT_EQ(0,
+ LDBCommandRunner::RunCommand(4, argv, opts, LDBOptions(), nullptr));
+
+ ASSERT_OK(DB::Open(opts, dbname, &db));
+
+ // Verify the checksum information in memory is the same as that in Manifest;
+ std::vector<LiveFileMetaData> live_files;
+ db->GetLiveFilesMetaData(&live_files);
+ delete db;
+ ASSERT_OK(fct_helper_ac.VerifyChecksumInManifest(live_files));
+}
+
+TEST_F(LdbCmdTest, BlobDBDumpFileChecksumNoChecksum) {
+ Env* base_env = TryLoadCustomOrDefaultEnv();
+ std::unique_ptr<Env> env(NewMemEnv(base_env));
+ Options opts;
+ opts.env = env.get();
+ opts.create_if_missing = true;
+ opts.enable_blob_files = true;
+
+ DB* db = nullptr;
+ std::string dbname = test::PerThreadDBPath(env.get(), "ldb_cmd_test");
+ ASSERT_OK(DB::Open(opts, dbname, &db));
+
+ WriteOptions wopts;
+ FlushOptions fopts;
+ fopts.wait = true;
+ Random rnd(test::RandomSeed());
+ for (int i = 0; i < 200; i++) {
+ std::ostringstream oss;
+ oss << std::setfill('0') << std::setw(8) << std::fixed << i;
+ std::string v = rnd.RandomString(100);
+ ASSERT_OK(db->Put(wopts, oss.str(), v));
+ }
+ ASSERT_OK(db->Flush(fopts));
+ for (int i = 100; i < 300; i++) {
+ std::ostringstream oss;
+ oss << std::setfill('0') << std::setw(8) << std::fixed << i;
+ std::string v = rnd.RandomString(100);
+ ASSERT_OK(db->Put(wopts, oss.str(), v));
+ }
+ ASSERT_OK(db->Flush(fopts));
+ for (int i = 200; i < 400; i++) {
+ std::ostringstream oss;
+ oss << std::setfill('0') << std::setw(8) << std::fixed << i;
+ std::string v = rnd.RandomString(100);
+ ASSERT_OK(db->Put(wopts, oss.str(), v));
+ }
+ ASSERT_OK(db->Flush(fopts));
+ for (int i = 300; i < 400; i++) {
+ std::ostringstream oss;
+ oss << std::setfill('0') << std::setw(8) << std::fixed << i;
+ std::string v = rnd.RandomString(100);
+ ASSERT_OK(db->Put(wopts, oss.str(), v));
+ }
+ ASSERT_OK(db->Flush(fopts));
+ ASSERT_OK(db->Close());
+ delete db;
+
+ char arg1[] = "./ldb";
+ std::string arg2_str = "--db=" + dbname;
+ char arg3[] = "file_checksum_dump";
+ char arg4[] = "--hex";
+ char* argv[] = {arg1, const_cast<char*>(arg2_str.c_str()), arg3, arg4};
+
+ ASSERT_EQ(0,
+ LDBCommandRunner::RunCommand(4, argv, opts, LDBOptions(), nullptr));
+
+ ASSERT_OK(DB::Open(opts, dbname, &db));
+
+ // Verify each sst and blob file checksum value and checksum name
+ FileChecksumTestHelper fct_helper(opts, db, dbname);
+ ASSERT_OK(fct_helper.VerifyEachFileChecksum());
+
+ // Manually trigger compaction
+ std::ostringstream oss_b_buf;
+ oss_b_buf << std::setfill('0') << std::setw(8) << std::fixed << 0;
+ std::ostringstream oss_e_buf;
+ oss_e_buf << std::setfill('0') << std::setw(8) << std::fixed << 399;
+ std::string b_buf = oss_b_buf.str();
+ std::string e_buf = oss_e_buf.str();
+ Slice begin(b_buf);
+ Slice end(e_buf);
+
+ CompactRangeOptions options;
+ ASSERT_OK(db->CompactRange(options, &begin, &end));
+ // Verify each sst file checksum after compaction
+ FileChecksumTestHelper fct_helper_ac(opts, db, dbname);
+ ASSERT_OK(fct_helper_ac.VerifyEachFileChecksum());
+
+ ASSERT_OK(db->Close());
+ delete db;
+
+ ASSERT_EQ(0,
+ LDBCommandRunner::RunCommand(4, argv, opts, LDBOptions(), nullptr));
+}
+
+TEST_F(LdbCmdTest, DumpFileChecksumCRC32) {
+ Env* base_env = TryLoadCustomOrDefaultEnv();
+ std::unique_ptr<Env> env(NewMemEnv(base_env));
+ Options opts;
+ opts.env = env.get();
+ opts.create_if_missing = true;
+ opts.file_checksum_gen_factory = GetFileChecksumGenCrc32cFactory();
+
+ DB* db = nullptr;
+ std::string dbname = test::PerThreadDBPath(env.get(), "ldb_cmd_test");
+ ASSERT_OK(DB::Open(opts, dbname, &db));
+
+ WriteOptions wopts;
+ FlushOptions fopts;
+ fopts.wait = true;
+ Random rnd(test::RandomSeed());
+ for (int i = 0; i < 100; i++) {
+ char buf[16];
+ snprintf(buf, sizeof(buf), "%08d", i);
+ std::string v = rnd.RandomString(100);
+ ASSERT_OK(db->Put(wopts, buf, v));
+ }
+ ASSERT_OK(db->Flush(fopts));
+ for (int i = 50; i < 150; i++) {
+ char buf[16];
+ snprintf(buf, sizeof(buf), "%08d", i);
+ std::string v = rnd.RandomString(100);
+ ASSERT_OK(db->Put(wopts, buf, v));
+ }
+ ASSERT_OK(db->Flush(fopts));
+ for (int i = 100; i < 200; i++) {
+ char buf[16];
+ snprintf(buf, sizeof(buf), "%08d", i);
+ std::string v = rnd.RandomString(100);
+ ASSERT_OK(db->Put(wopts, buf, v));
+ }
+ ASSERT_OK(db->Flush(fopts));
+ for (int i = 150; i < 250; i++) {
+ char buf[16];
+ snprintf(buf, sizeof(buf), "%08d", i);
+ std::string v = rnd.RandomString(100);
+ ASSERT_OK(db->Put(wopts, buf, v));
+ }
+ ASSERT_OK(db->Flush(fopts));
+ ASSERT_OK(db->Close());
+ delete db;
+
+ char arg1[] = "./ldb";
+ char arg2[1024];
+ snprintf(arg2, sizeof(arg2), "--db=%s", dbname.c_str());
+ char arg3[] = "file_checksum_dump";
+ char arg4[] = "--hex";
+ char* argv[] = {arg1, arg2, arg3, arg4};
+
+ ASSERT_EQ(0,
+ LDBCommandRunner::RunCommand(4, argv, opts, LDBOptions(), nullptr));
+
+ ASSERT_OK(DB::Open(opts, dbname, &db));
+
+ // Verify each sst file checksum value and checksum name
+ FileChecksumTestHelper fct_helper(opts, db, dbname);
+ ASSERT_OK(fct_helper.VerifyEachFileChecksum());
+
+ // Manually trigger compaction
+ char b_buf[16];
+ snprintf(b_buf, sizeof(b_buf), "%08d", 0);
+ char e_buf[16];
+ snprintf(e_buf, sizeof(e_buf), "%08d", 249);
+ Slice begin(b_buf);
+ Slice end(e_buf);
+ CompactRangeOptions options;
+ ASSERT_OK(db->CompactRange(options, &begin, &end));
+ // Verify each sst file checksum after compaction
+ FileChecksumTestHelper fct_helper_ac(opts, db, dbname);
+ ASSERT_OK(fct_helper_ac.VerifyEachFileChecksum());
+
+ ASSERT_OK(db->Close());
+ delete db;
+
+ ASSERT_EQ(0,
+ LDBCommandRunner::RunCommand(4, argv, opts, LDBOptions(), nullptr));
+
+ ASSERT_OK(DB::Open(opts, dbname, &db));
+
+ // Verify the checksum information in memory is the same as that in Manifest;
+ std::vector<LiveFileMetaData> live_files;
+ db->GetLiveFilesMetaData(&live_files);
+ ASSERT_OK(fct_helper_ac.VerifyChecksumInManifest(live_files));
+
+ ASSERT_OK(db->Close());
+ delete db;
+}
+
+TEST_F(LdbCmdTest, BlobDBDumpFileChecksumCRC32) {
+ Env* base_env = TryLoadCustomOrDefaultEnv();
+ std::unique_ptr<Env> env(NewMemEnv(base_env));
+ Options opts;
+ opts.env = env.get();
+ opts.create_if_missing = true;
+ opts.file_checksum_gen_factory = GetFileChecksumGenCrc32cFactory();
+ opts.enable_blob_files = true;
+
+ DB* db = nullptr;
+ std::string dbname = test::PerThreadDBPath(env.get(), "ldb_cmd_test");
+ ASSERT_OK(DB::Open(opts, dbname, &db));
+
+ WriteOptions wopts;
+ FlushOptions fopts;
+ fopts.wait = true;
+ Random rnd(test::RandomSeed());
+ for (int i = 0; i < 100; i++) {
+ std::ostringstream oss;
+ oss << std::setfill('0') << std::setw(8) << std::fixed << i;
+ std::string v = rnd.RandomString(100);
+ ASSERT_OK(db->Put(wopts, oss.str(), v));
+ }
+ ASSERT_OK(db->Flush(fopts));
+ for (int i = 50; i < 150; i++) {
+ std::ostringstream oss;
+ oss << std::setfill('0') << std::setw(8) << std::fixed << i;
+ std::string v = rnd.RandomString(100);
+ ASSERT_OK(db->Put(wopts, oss.str(), v));
+ }
+ ASSERT_OK(db->Flush(fopts));
+ for (int i = 100; i < 200; i++) {
+ std::ostringstream oss;
+ oss << std::setfill('0') << std::setw(8) << std::fixed << i;
+ std::string v = rnd.RandomString(100);
+ ASSERT_OK(db->Put(wopts, oss.str(), v));
+ }
+ ASSERT_OK(db->Flush(fopts));
+ for (int i = 150; i < 250; i++) {
+ std::ostringstream oss;
+ oss << std::setfill('0') << std::setw(8) << std::fixed << i;
+ std::string v = rnd.RandomString(100);
+ ASSERT_OK(db->Put(wopts, oss.str(), v));
+ }
+ ASSERT_OK(db->Flush(fopts));
+ ASSERT_OK(db->Close());
+ delete db;
+
+ char arg1[] = "./ldb";
+ std::string arg2_str = "--db=" + dbname;
+ char arg3[] = "file_checksum_dump";
+ char arg4[] = "--hex";
+ char* argv[] = {arg1, const_cast<char*>(arg2_str.c_str()), arg3, arg4};
+
+ ASSERT_EQ(0,
+ LDBCommandRunner::RunCommand(4, argv, opts, LDBOptions(), nullptr));
+
+ ASSERT_OK(DB::Open(opts, dbname, &db));
+
+ // Verify each sst and blob file checksum value and checksum name
+ FileChecksumTestHelper fct_helper(opts, db, dbname);
+ ASSERT_OK(fct_helper.VerifyEachFileChecksum());
+
+ // Manually trigger compaction
+ std::ostringstream oss_b_buf;
+ oss_b_buf << std::setfill('0') << std::setw(8) << std::fixed << 0;
+ std::ostringstream oss_e_buf;
+ oss_e_buf << std::setfill('0') << std::setw(8) << std::fixed << 249;
+ std::string b_buf = oss_b_buf.str();
+ std::string e_buf = oss_e_buf.str();
+ Slice begin(b_buf);
+ Slice end(e_buf);
+
+ CompactRangeOptions options;
+ ASSERT_OK(db->CompactRange(options, &begin, &end));
+ // Verify each sst file checksum after compaction
+ FileChecksumTestHelper fct_helper_ac(opts, db, dbname);
+ ASSERT_OK(fct_helper_ac.VerifyEachFileChecksum());
+
+ ASSERT_OK(db->Close());
+ delete db;
+
+ ASSERT_EQ(0,
+ LDBCommandRunner::RunCommand(4, argv, opts, LDBOptions(), nullptr));
+}
+
+TEST_F(LdbCmdTest, OptionParsing) {
+ // test parsing flags
+ Options opts;
+ opts.env = TryLoadCustomOrDefaultEnv();
+ {
+ std::vector<std::string> args;
+ args.push_back("scan");
+ args.push_back("--ttl");
+ args.push_back("--timestamp");
+ LDBCommand* command = ROCKSDB_NAMESPACE::LDBCommand::InitFromCmdLineArgs(
+ args, opts, LDBOptions(), nullptr);
+ const std::vector<std::string> flags = command->TEST_GetFlags();
+ EXPECT_EQ(flags.size(), 2);
+ EXPECT_EQ(flags[0], "ttl");
+ EXPECT_EQ(flags[1], "timestamp");
+ delete command;
+ }
+ // test parsing options which contains equal sign in the option value
+ {
+ std::vector<std::string> args;
+ args.push_back("scan");
+ args.push_back("--db=/dev/shm/ldbtest/");
+ args.push_back(
+ "--from='abcd/efg/hijk/lmn/"
+ "opq:__rst.uvw.xyz?a=3+4+bcd+efghi&jk=lm_no&pq=rst-0&uv=wx-8&yz=a&bcd_"
+ "ef=gh.ijk'");
+ LDBCommand* command = ROCKSDB_NAMESPACE::LDBCommand::InitFromCmdLineArgs(
+ args, opts, LDBOptions(), nullptr);
+ const std::map<std::string, std::string> option_map =
+ command->TEST_GetOptionMap();
+ EXPECT_EQ(option_map.at("db"), "/dev/shm/ldbtest/");
+ EXPECT_EQ(option_map.at("from"),
+ "'abcd/efg/hijk/lmn/"
+ "opq:__rst.uvw.xyz?a=3+4+bcd+efghi&jk=lm_no&pq=rst-0&uv=wx-8&yz="
+ "a&bcd_ef=gh.ijk'");
+ delete command;
+ }
+}
+
+TEST_F(LdbCmdTest, ListFileTombstone) {
+ Env* base_env = TryLoadCustomOrDefaultEnv();
+ std::unique_ptr<Env> env(NewMemEnv(base_env));
+ Options opts;
+ opts.env = env.get();
+ opts.create_if_missing = true;
+
+ DB* db = nullptr;
+ std::string dbname = test::PerThreadDBPath(env.get(), "ldb_cmd_test");
+ ASSERT_OK(DB::Open(opts, dbname, &db));
+
+ WriteOptions wopts;
+ ASSERT_OK(db->Put(wopts, "foo", "1"));
+ ASSERT_OK(db->Put(wopts, "bar", "2"));
+
+ FlushOptions fopts;
+ fopts.wait = true;
+ ASSERT_OK(db->Flush(fopts));
+
+ ASSERT_OK(db->DeleteRange(wopts, db->DefaultColumnFamily(), "foo", "foo2"));
+ ASSERT_OK(db->DeleteRange(wopts, db->DefaultColumnFamily(), "bar", "foo2"));
+ ASSERT_OK(db->Flush(fopts));
+
+ delete db;
+
+ {
+ char arg1[] = "./ldb";
+ char arg2[1024];
+ snprintf(arg2, sizeof(arg2), "--db=%s", dbname.c_str());
+ char arg3[] = "list_file_range_deletes";
+ char* argv[] = {arg1, arg2, arg3};
+
+ ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
+ "ListFileRangeDeletesCommand::DoCommand:BeforePrint", [&](void* arg) {
+ std::string* out_str = reinterpret_cast<std::string*>(arg);
+
+ // Count number of tombstones printed
+ int num_tb = 0;
+ const std::string kFingerprintStr = "start: ";
+ auto offset = out_str->find(kFingerprintStr);
+ while (offset != std::string::npos) {
+ num_tb++;
+ offset =
+ out_str->find(kFingerprintStr, offset + kFingerprintStr.size());
+ }
+ EXPECT_EQ(2, num_tb);
+ });
+ ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
+
+ ASSERT_EQ(
+ 0, LDBCommandRunner::RunCommand(3, argv, opts, LDBOptions(), nullptr));
+
+ ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
+ ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
+ }
+
+ // Test the case of limiting tombstones
+ {
+ char arg1[] = "./ldb";
+ char arg2[1024];
+ snprintf(arg2, sizeof(arg2), "--db=%s", dbname.c_str());
+ char arg3[] = "list_file_range_deletes";
+ char arg4[] = "--max_keys=1";
+ char* argv[] = {arg1, arg2, arg3, arg4};
+
+ ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
+ "ListFileRangeDeletesCommand::DoCommand:BeforePrint", [&](void* arg) {
+ std::string* out_str = reinterpret_cast<std::string*>(arg);
+
+ // Count number of tombstones printed
+ int num_tb = 0;
+ const std::string kFingerprintStr = "start: ";
+ auto offset = out_str->find(kFingerprintStr);
+ while (offset != std::string::npos) {
+ num_tb++;
+ offset =
+ out_str->find(kFingerprintStr, offset + kFingerprintStr.size());
+ }
+ EXPECT_EQ(1, num_tb);
+ });
+ ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
+
+ ASSERT_EQ(
+ 0, LDBCommandRunner::RunCommand(4, argv, opts, LDBOptions(), nullptr));
+
+ ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
+ ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
+ }
+}
+
+TEST_F(LdbCmdTest, DisableConsistencyChecks) {
+ Env* base_env = TryLoadCustomOrDefaultEnv();
+ std::unique_ptr<Env> env(NewMemEnv(base_env));
+ Options opts;
+ opts.env = env.get();
+ opts.create_if_missing = true;
+
+ std::string dbname = test::PerThreadDBPath(env.get(), "ldb_cmd_test");
+
+ {
+ DB* db = nullptr;
+ ASSERT_OK(DB::Open(opts, dbname, &db));
+
+ WriteOptions wopts;
+ FlushOptions fopts;
+ fopts.wait = true;
+
+ ASSERT_OK(db->Put(wopts, "foo1", "1"));
+ ASSERT_OK(db->Put(wopts, "bar1", "2"));
+ ASSERT_OK(db->Flush(fopts));
+
+ ASSERT_OK(db->Put(wopts, "foo2", "3"));
+ ASSERT_OK(db->Put(wopts, "bar2", "4"));
+ ASSERT_OK(db->Flush(fopts));
+
+ delete db;
+ }
+
+ {
+ char arg1[] = "./ldb";
+ char arg2[1024];
+ snprintf(arg2, sizeof(arg2), "--db=%s", dbname.c_str());
+ char arg3[] = "checkconsistency";
+ char* argv[] = {arg1, arg2, arg3};
+
+ SyncPoint::GetInstance()->SetCallBack(
+ "Version::PrepareAppend:forced_check", [&](void* arg) {
+ bool* forced = reinterpret_cast<bool*>(arg);
+ ASSERT_TRUE(*forced);
+ });
+ SyncPoint::GetInstance()->EnableProcessing();
+
+ ASSERT_EQ(
+ 0, LDBCommandRunner::RunCommand(3, argv, opts, LDBOptions(), nullptr));
+
+ SyncPoint::GetInstance()->ClearAllCallBacks();
+ SyncPoint::GetInstance()->DisableProcessing();
+ }
+ {
+ char arg1[] = "./ldb";
+ char arg2[1024];
+ snprintf(arg2, sizeof(arg2), "--db=%s", dbname.c_str());
+ char arg3[] = "scan";
+ char* argv[] = {arg1, arg2, arg3};
+
+ SyncPoint::GetInstance()->SetCallBack(
+ "Version::PrepareAppend:forced_check", [&](void* arg) {
+ bool* forced = reinterpret_cast<bool*>(arg);
+ ASSERT_TRUE(*forced);
+ });
+ SyncPoint::GetInstance()->EnableProcessing();
+
+ ASSERT_EQ(
+ 0, LDBCommandRunner::RunCommand(3, argv, opts, LDBOptions(), nullptr));
+
+ SyncPoint::GetInstance()->ClearAllCallBacks();
+ SyncPoint::GetInstance()->DisableProcessing();
+ }
+ {
+ char arg1[] = "./ldb";
+ char arg2[1024];
+ snprintf(arg2, sizeof(arg2), "--db=%s", dbname.c_str());
+ char arg3[] = "scan";
+ char arg4[] = "--disable_consistency_checks";
+ char* argv[] = {arg1, arg2, arg3, arg4};
+
+ SyncPoint::GetInstance()->SetCallBack(
+ "ColumnFamilyData::ColumnFamilyData", [&](void* arg) {
+ ColumnFamilyOptions* cfo =
+ reinterpret_cast<ColumnFamilyOptions*>(arg);
+ ASSERT_FALSE(cfo->force_consistency_checks);
+ });
+ SyncPoint::GetInstance()->EnableProcessing();
+
+ ASSERT_EQ(
+ 0, LDBCommandRunner::RunCommand(4, argv, opts, LDBOptions(), nullptr));
+
+ SyncPoint::GetInstance()->ClearAllCallBacks();
+ SyncPoint::GetInstance()->DisableProcessing();
+ }
+}
+
+TEST_F(LdbCmdTest, TestBadDbPath) {
+ Env* base_env = TryLoadCustomOrDefaultEnv();
+ std::unique_ptr<Env> env(NewMemEnv(base_env));
+ Options opts;
+ opts.env = env.get();
+ opts.create_if_missing = true;
+
+ std::string dbname = test::PerThreadDBPath(env.get(), "ldb_cmd_test");
+ char arg1[] = "./ldb";
+ char arg2[1024];
+ snprintf(arg2, sizeof(arg2), "--db=%s/.no_such_dir", dbname.c_str());
+ char arg3[1024];
+ snprintf(arg3, sizeof(arg3), "create_column_family");
+ char arg4[] = "bad cf";
+ char* argv[] = {arg1, arg2, arg3, arg4};
+
+ ASSERT_EQ(1,
+ LDBCommandRunner::RunCommand(4, argv, opts, LDBOptions(), nullptr));
+ snprintf(arg3, sizeof(arg3), "drop_column_family");
+ ASSERT_EQ(1,
+ LDBCommandRunner::RunCommand(4, argv, opts, LDBOptions(), nullptr));
+}
+namespace {
+class WrappedEnv : public EnvWrapper {
+ public:
+ explicit WrappedEnv(Env* t) : EnvWrapper(t) {}
+ static const char* kClassName() { return "WrappedEnv"; }
+ const char* Name() const override { return kClassName(); }
+};
+} // namespace
+TEST_F(LdbCmdTest, LoadCFOptionsAndOverride) {
+ // Env* base_env = TryLoadCustomOrDefaultEnv();
+ // std::unique_ptr<Env> env(NewMemEnv(base_env));
+ std::unique_ptr<Env> env(new WrappedEnv(Env::Default()));
+ Options opts;
+ opts.env = env.get();
+ opts.create_if_missing = true;
+
+ DB* db = nullptr;
+ std::string dbname = test::PerThreadDBPath(env.get(), "ldb_cmd_test");
+ ASSERT_OK(DestroyDB(dbname, opts));
+ ASSERT_OK(DB::Open(opts, dbname, &db));
+
+ ColumnFamilyHandle* cf_handle;
+ ColumnFamilyOptions cf_opts;
+ cf_opts.num_levels = 20;
+ ASSERT_OK(db->CreateColumnFamily(cf_opts, "cf1", &cf_handle));
+
+ delete cf_handle;
+ delete db;
+
+ char arg1[] = "./ldb";
+ char arg2[1024];
+ snprintf(arg2, sizeof(arg2), "--db=%s", dbname.c_str());
+ char arg3[] = "put";
+ char arg4[] = "key1";
+ char arg5[] = "value1";
+ char arg6[] = "--try_load_options";
+ char arg7[] = "--column_family=cf1";
+ char arg8[] = "--write_buffer_size=268435456";
+ char* argv[] = {arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8};
+
+ ASSERT_EQ(0,
+ LDBCommandRunner::RunCommand(8, argv, opts, LDBOptions(), nullptr));
+
+ ConfigOptions config_opts;
+ Options options;
+ std::vector<ColumnFamilyDescriptor> column_families;
+ config_opts.env = env.get();
+ ASSERT_OK(LoadLatestOptions(config_opts, dbname, &options, &column_families));
+ ASSERT_EQ(column_families.size(), 2);
+ ASSERT_EQ(options.num_levels, opts.num_levels);
+ ASSERT_EQ(column_families[1].options.num_levels, cf_opts.num_levels);
+ ASSERT_EQ(column_families[1].options.write_buffer_size, 268435456);
+}
+
+TEST_F(LdbCmdTest, UnsafeRemoveSstFile) {
+ Options opts;
+ opts.level0_file_num_compaction_trigger = 10;
+ opts.create_if_missing = true;
+
+ DB* db = nullptr;
+ std::string dbname = test::PerThreadDBPath(Env::Default(), "ldb_cmd_test");
+ ASSERT_OK(DestroyDB(dbname, opts));
+ ASSERT_OK(DB::Open(opts, dbname, &db));
+
+ // Create three SST files
+ for (size_t i = 0; i < 3; ++i) {
+ ASSERT_OK(db->Put(WriteOptions(), std::to_string(i), std::to_string(i)));
+ ASSERT_OK(db->Flush(FlushOptions()));
+ }
+
+ // Determine which is the "middle" one
+ std::vector<LiveFileMetaData> sst_files;
+ db->GetLiveFilesMetaData(&sst_files);
+
+ std::vector<uint64_t> numbers;
+ for (auto& f : sst_files) {
+ numbers.push_back(f.file_number);
+ }
+ ASSERT_EQ(numbers.size(), 3);
+ std::sort(numbers.begin(), numbers.end());
+ uint64_t to_remove = numbers[1];
+
+ // Close for unsafe_remove_sst_file
+ delete db;
+ db = nullptr;
+
+ char arg1[] = "./ldb";
+ char arg2[1024];
+ snprintf(arg2, sizeof(arg2), "--db=%s", dbname.c_str());
+ char arg3[] = "unsafe_remove_sst_file";
+ char arg4[20];
+ snprintf(arg4, sizeof(arg4), "%" PRIu64, to_remove);
+ char* argv[] = {arg1, arg2, arg3, arg4};
+
+ ASSERT_EQ(0,
+ LDBCommandRunner::RunCommand(4, argv, opts, LDBOptions(), nullptr));
+
+ // Re-open, and verify with Get that middle file is gone
+ ASSERT_OK(DB::Open(opts, dbname, &db));
+
+ std::string val;
+ ASSERT_OK(db->Get(ReadOptions(), "0", &val));
+ ASSERT_EQ(val, "0");
+
+ ASSERT_OK(db->Get(ReadOptions(), "2", &val));
+ ASSERT_EQ(val, "2");
+
+ ASSERT_TRUE(db->Get(ReadOptions(), "1", &val).IsNotFound());
+
+ // Now with extra CF, two more files
+ ColumnFamilyHandle* cf_handle;
+ ColumnFamilyOptions cf_opts;
+ ASSERT_OK(db->CreateColumnFamily(cf_opts, "cf1", &cf_handle));
+ for (size_t i = 3; i < 5; ++i) {
+ ASSERT_OK(db->Put(WriteOptions(), cf_handle, std::to_string(i),
+ std::to_string(i)));
+ ASSERT_OK(db->Flush(FlushOptions(), cf_handle));
+ }
+
+ // Determine which is the "last" one
+ sst_files.clear();
+ db->GetLiveFilesMetaData(&sst_files);
+
+ numbers.clear();
+ for (auto& f : sst_files) {
+ numbers.push_back(f.file_number);
+ }
+ ASSERT_EQ(numbers.size(), 4);
+ std::sort(numbers.begin(), numbers.end());
+ to_remove = numbers.back();
+
+ // Close for unsafe_remove_sst_file
+ delete cf_handle;
+ delete db;
+ db = nullptr;
+
+ snprintf(arg4, sizeof(arg4), "%" PRIu64, to_remove);
+ ASSERT_EQ(0,
+ LDBCommandRunner::RunCommand(4, argv, opts, LDBOptions(), nullptr));
+
+ std::vector<ColumnFamilyDescriptor> cfds = {{kDefaultColumnFamilyName, opts},
+ {"cf1", cf_opts}};
+ std::vector<ColumnFamilyHandle*> handles;
+ ASSERT_OK(DB::Open(opts, dbname, cfds, &handles, &db));
+
+ ASSERT_OK(db->Get(ReadOptions(), handles[1], "3", &val));
+ ASSERT_EQ(val, "3");
+
+ ASSERT_TRUE(db->Get(ReadOptions(), handles[1], "4", &val).IsNotFound());
+
+ ASSERT_OK(db->Get(ReadOptions(), handles[0], "0", &val));
+ ASSERT_EQ(val, "0");
+
+ // Determine which is the "first" one (most likely to be opened in recovery)
+ sst_files.clear();
+ db->GetLiveFilesMetaData(&sst_files);
+
+ numbers.clear();
+ for (auto& f : sst_files) {
+ numbers.push_back(f.file_number);
+ }
+ ASSERT_EQ(numbers.size(), 3);
+ std::sort(numbers.begin(), numbers.end());
+ to_remove = numbers.front();
+
+ // This time physically delete the file before unsafe_remove
+ {
+ std::string f = dbname + "/" + MakeTableFileName(to_remove);
+ ASSERT_OK(Env::Default()->DeleteFile(f));
+ }
+
+ // Close for unsafe_remove_sst_file
+ for (auto& h : handles) {
+ delete h;
+ }
+ delete db;
+ db = nullptr;
+
+ snprintf(arg4, sizeof(arg4), "%" PRIu64, to_remove);
+ ASSERT_EQ(0,
+ LDBCommandRunner::RunCommand(4, argv, opts, LDBOptions(), nullptr));
+
+ ASSERT_OK(DB::Open(opts, dbname, cfds, &handles, &db));
+
+ ASSERT_OK(db->Get(ReadOptions(), handles[1], "3", &val));
+ ASSERT_EQ(val, "3");
+
+ ASSERT_TRUE(db->Get(ReadOptions(), handles[0], "0", &val).IsNotFound());
+
+ for (auto& h : handles) {
+ delete h;
+ }
+ delete db;
+}
+
+TEST_F(LdbCmdTest, FileTemperatureUpdateManifest) {
+ auto test_fs = std::make_shared<FileTemperatureTestFS>(FileSystem::Default());
+ std::unique_ptr<Env> env(new CompositeEnvWrapper(Env::Default(), test_fs));
+ Options opts;
+ opts.bottommost_temperature = Temperature::kWarm;
+ opts.level0_file_num_compaction_trigger = 10;
+ opts.create_if_missing = true;
+ opts.env = env.get();
+
+ DB* db = nullptr;
+ std::string dbname = test::PerThreadDBPath(env.get(), "ldb_cmd_test");
+ ASSERT_OK(DestroyDB(dbname, opts));
+ ASSERT_OK(DB::Open(opts, dbname, &db));
+
+ std::array<Temperature, 5> kTestTemps = {
+ Temperature::kCold, Temperature::kWarm, Temperature::kHot,
+ Temperature::kWarm, Temperature::kCold};
+ std::map<uint64_t, Temperature> number_to_temp;
+ for (size_t i = 0; i < kTestTemps.size(); ++i) {
+ ASSERT_OK(db->Put(WriteOptions(), std::to_string(i), std::to_string(i)));
+ ASSERT_OK(db->Flush(FlushOptions()));
+
+ std::map<uint64_t, Temperature> current_temps;
+ test_fs->CopyCurrentSstFileTemperatures(&current_temps);
+ for (auto e : current_temps) {
+ if (e.second == Temperature::kUnknown) {
+ test_fs->OverrideSstFileTemperature(e.first, kTestTemps[i]);
+ number_to_temp[e.first] = kTestTemps[i];
+ }
+ }
+ }
+
+ // Close & reopen
+ delete db;
+ db = nullptr;
+ test_fs->PopRequestedSstFileTemperatures();
+ ASSERT_OK(DB::Open(opts, dbname, &db));
+
+ for (size_t i = 0; i < kTestTemps.size(); ++i) {
+ std::string val;
+ ASSERT_OK(db->Get(ReadOptions(), std::to_string(i), &val));
+ ASSERT_EQ(val, std::to_string(i));
+ }
+
+ // Still all unknown
+ std::vector<std::pair<uint64_t, Temperature>> requests;
+ test_fs->PopRequestedSstFileTemperatures(&requests);
+ ASSERT_EQ(requests.size(), kTestTemps.size());
+ for (auto& r : requests) {
+ ASSERT_EQ(r.second, Temperature::kUnknown);
+ }
+
+ // Close for update_manifest
+ delete db;
+ db = nullptr;
+
+ char arg1[] = "./ldb";
+ char arg2[1024];
+ snprintf(arg2, sizeof(arg2), "--db=%s", dbname.c_str());
+ char arg3[] = "update_manifest";
+ char arg4[] = "--update_temperatures";
+ char* argv[] = {arg1, arg2, arg3, arg4};
+
+ ASSERT_EQ(0,
+ LDBCommandRunner::RunCommand(4, argv, opts, LDBOptions(), nullptr));
+
+ // Re-open, get, and verify manifest temps (based on request)
+ test_fs->PopRequestedSstFileTemperatures();
+ ASSERT_OK(DB::Open(opts, dbname, &db));
+
+ for (size_t i = 0; i < kTestTemps.size(); ++i) {
+ std::string val;
+ ASSERT_OK(db->Get(ReadOptions(), std::to_string(i), &val));
+ ASSERT_EQ(val, std::to_string(i));
+ }
+
+ requests.clear();
+ test_fs->PopRequestedSstFileTemperatures(&requests);
+ ASSERT_EQ(requests.size(), kTestTemps.size());
+ for (auto& r : requests) {
+ ASSERT_EQ(r.second, number_to_temp[r.first]);
+ }
+ delete db;
+}
+
+TEST_F(LdbCmdTest, RenameDbAndLoadOptions) {
+ Env* env = TryLoadCustomOrDefaultEnv();
+ Options opts;
+ opts.env = env;
+ opts.create_if_missing = false;
+
+ std::string old_dbname = test::PerThreadDBPath(env, "ldb_cmd_test");
+ std::string new_dbname = old_dbname + "_2";
+ ASSERT_OK(DestroyDB(old_dbname, opts));
+ ASSERT_OK(DestroyDB(new_dbname, opts));
+
+ char old_arg[1024];
+ snprintf(old_arg, sizeof(old_arg), "--db=%s", old_dbname.c_str());
+ char new_arg[1024];
+ snprintf(new_arg, sizeof(old_arg), "--db=%s", new_dbname.c_str());
+ const char* argv1[] = {"./ldb",
+ old_arg,
+ "put",
+ "key1",
+ "value1",
+ "--try_load_options",
+ "--create_if_missing"};
+
+ const char* argv2[] = {"./ldb", old_arg, "get", "key1", "--try_load_options"};
+ const char* argv3[] = {"./ldb", new_arg, "put",
+ "key2", "value2", "--try_load_options"};
+
+ const char* argv4[] = {"./ldb", new_arg, "get", "key1", "--try_load_options"};
+ const char* argv5[] = {"./ldb", new_arg, "get", "key2", "--try_load_options"};
+
+ ASSERT_EQ(
+ 0, LDBCommandRunner::RunCommand(7, argv1, opts, LDBOptions(), nullptr));
+ ASSERT_EQ(
+ 0, LDBCommandRunner::RunCommand(5, argv2, opts, LDBOptions(), nullptr));
+ ConfigOptions config_opts;
+ Options options;
+ std::vector<ColumnFamilyDescriptor> column_families;
+ config_opts.env = env;
+ ASSERT_OK(
+ LoadLatestOptions(config_opts, old_dbname, &options, &column_families));
+ ASSERT_EQ(options.wal_dir, "");
+
+ ASSERT_OK(env->RenameFile(old_dbname, new_dbname));
+ ASSERT_NE(
+ 0, LDBCommandRunner::RunCommand(6, argv1, opts, LDBOptions(), nullptr));
+ ASSERT_NE(
+ 0, LDBCommandRunner::RunCommand(5, argv2, opts, LDBOptions(), nullptr));
+ ASSERT_EQ(
+ 0, LDBCommandRunner::RunCommand(6, argv3, opts, LDBOptions(), nullptr));
+ ASSERT_EQ(
+ 0, LDBCommandRunner::RunCommand(5, argv4, opts, LDBOptions(), nullptr));
+ ASSERT_EQ(
+ 0, LDBCommandRunner::RunCommand(5, argv5, opts, LDBOptions(), nullptr));
+ ASSERT_OK(DestroyDB(new_dbname, opts));
+}
+
+} // namespace ROCKSDB_NAMESPACE
+
+int main(int argc, char** argv) {
+ ROCKSDB_NAMESPACE::port::InstallStackTraceHandler();
+ ::testing::InitGoogleTest(&argc, argv);
+ RegisterCustomObjects(argc, argv);
+ return RUN_ALL_TESTS();
+}
+#else
+#include <stdio.h>
+
+int main(int /*argc*/, char** /*argv*/) {
+ fprintf(stderr, "SKIPPED as LDBCommand is not supported in ROCKSDB_LITE\n");
+ return 0;
+}
+
+#endif // ROCKSDB_LITE
diff --git a/src/rocksdb/tools/ldb_test.py b/src/rocksdb/tools/ldb_test.py
new file mode 100644
index 000000000..e243d69c0
--- /dev/null
+++ b/src/rocksdb/tools/ldb_test.py
@@ -0,0 +1,955 @@
+#!/usr/bin/env python3
+# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+import glob
+
+import os
+import os.path
+import re
+import shutil
+import subprocess
+import tempfile
+import time
+import unittest
+
+
+def my_check_output(*popenargs, **kwargs):
+ """
+ If we had python 2.7, we should simply use subprocess.check_output.
+ This is a stop-gap solution for python 2.6
+ """
+ if "stdout" in kwargs:
+ raise ValueError("stdout argument not allowed, it will be overridden.")
+ process = subprocess.Popen(
+ stderr=subprocess.PIPE, stdout=subprocess.PIPE, *popenargs, **kwargs
+ )
+ output, unused_err = process.communicate()
+ retcode = process.poll()
+ if retcode:
+ cmd = kwargs.get("args")
+ if cmd is None:
+ cmd = popenargs[0]
+ raise Exception("Exit code is not 0. It is %d. Command: %s" % (retcode, cmd))
+ return output.decode("utf-8")
+
+
+def run_err_null(cmd):
+ return os.system(cmd + " 2>/dev/null ")
+
+
+class LDBTestCase(unittest.TestCase):
+ def setUp(self):
+ self.TMP_DIR = tempfile.mkdtemp(prefix="ldb_test_")
+ self.DB_NAME = "testdb"
+
+ def tearDown(self):
+ assert (
+ self.TMP_DIR.strip() != "/"
+ and self.TMP_DIR.strip() != "/tmp"
+ and self.TMP_DIR.strip() != "/tmp/"
+ ) # Just some paranoia
+
+ shutil.rmtree(self.TMP_DIR)
+
+ def dbParam(self, dbName):
+ return "--db=%s" % os.path.join(self.TMP_DIR, dbName)
+
+ def assertRunOKFull(
+ self, params, expectedOutput, unexpected=False, isPattern=False
+ ):
+ """
+ All command-line params must be specified.
+ Allows full flexibility in testing; for example: missing db param.
+ """
+ output = my_check_output(
+ './ldb %s |grep -v "Created bg thread"' % params, shell=True
+ )
+ if not unexpected:
+ if isPattern:
+ self.assertNotEqual(expectedOutput.search(output.strip()), None)
+ else:
+ self.assertEqual(output.strip(), expectedOutput.strip())
+ else:
+ if isPattern:
+ self.assertEqual(expectedOutput.search(output.strip()), None)
+ else:
+ self.assertNotEqual(output.strip(), expectedOutput.strip())
+
+ def assertRunFAILFull(self, params):
+ """
+ All command-line params must be specified.
+ Allows full flexibility in testing; for example: missing db param.
+ """
+ try:
+
+ my_check_output(
+ './ldb %s >/dev/null 2>&1 |grep -v "Created bg \
+ thread"'
+ % params,
+ shell=True,
+ )
+ except Exception:
+ return
+ self.fail(
+ "Exception should have been raised for command with params: %s" % params
+ )
+
+ def assertRunOK(self, params, expectedOutput, unexpected=False):
+ """
+ Uses the default test db.
+ """
+ self.assertRunOKFull(
+ "%s %s" % (self.dbParam(self.DB_NAME), params), expectedOutput, unexpected
+ )
+
+ def assertRunFAIL(self, params):
+ """
+ Uses the default test db.
+ """
+ self.assertRunFAILFull("%s %s" % (self.dbParam(self.DB_NAME), params))
+
+ def testSimpleStringPutGet(self):
+ print("Running testSimpleStringPutGet...")
+ self.assertRunFAIL("put x1 y1")
+ self.assertRunOK("put --create_if_missing x1 y1", "OK")
+ self.assertRunOK("get x1", "y1")
+ self.assertRunFAIL("get x2")
+
+ self.assertRunOK("put x2 y2", "OK")
+ self.assertRunOK("get x1", "y1")
+ self.assertRunOK("get x2", "y2")
+ self.assertRunFAIL("get x3")
+
+ self.assertRunOK("scan --from=x1 --to=z", "x1 : y1\nx2 : y2")
+ self.assertRunOK("put x3 y3", "OK")
+
+ self.assertRunOK("scan --from=x1 --to=z", "x1 : y1\nx2 : y2\nx3 : y3")
+ self.assertRunOK("scan", "x1 : y1\nx2 : y2\nx3 : y3")
+ self.assertRunOK("scan --from=x", "x1 : y1\nx2 : y2\nx3 : y3")
+
+ self.assertRunOK("scan --to=x2", "x1 : y1")
+ self.assertRunOK("scan --from=x1 --to=z --max_keys=1", "x1 : y1")
+ self.assertRunOK("scan --from=x1 --to=z --max_keys=2", "x1 : y1\nx2 : y2")
+
+ self.assertRunOK(
+ "scan --from=x1 --to=z --max_keys=3", "x1 : y1\nx2 : y2\nx3 : y3"
+ )
+ self.assertRunOK(
+ "scan --from=x1 --to=z --max_keys=4", "x1 : y1\nx2 : y2\nx3 : y3"
+ )
+ self.assertRunOK("scan --from=x1 --to=x2", "x1 : y1")
+ self.assertRunOK("scan --from=x2 --to=x4", "x2 : y2\nx3 : y3")
+ self.assertRunFAIL("scan --from=x4 --to=z") # No results => FAIL
+ self.assertRunFAIL("scan --from=x1 --to=z --max_keys=foo")
+
+ self.assertRunOK("scan", "x1 : y1\nx2 : y2\nx3 : y3")
+
+ self.assertRunOK("delete x1", "OK")
+ self.assertRunOK("scan", "x2 : y2\nx3 : y3")
+
+ self.assertRunOK("delete NonExistentKey", "OK")
+ # It is weird that GET and SCAN raise exception for
+ # non-existent key, while delete does not
+
+ self.assertRunOK("checkconsistency", "OK")
+
+ def dumpDb(self, params, dumpFile):
+ return 0 == run_err_null("./ldb dump %s > %s" % (params, dumpFile))
+
+ def loadDb(self, params, dumpFile):
+ return 0 == run_err_null("cat %s | ./ldb load %s" % (dumpFile, params))
+
+ def writeExternSst(self, params, inputDumpFile, outputSst):
+ return 0 == run_err_null(
+ "cat %s | ./ldb write_extern_sst %s %s" % (inputDumpFile, outputSst, params)
+ )
+
+ def ingestExternSst(self, params, inputSst):
+ return 0 == run_err_null("./ldb ingest_extern_sst %s %s" % (inputSst, params))
+
+ def testStringBatchPut(self):
+ print("Running testStringBatchPut...")
+ self.assertRunOK("batchput x1 y1 --create_if_missing", "OK")
+ self.assertRunOK("scan", "x1 : y1")
+ self.assertRunOK('batchput x2 y2 x3 y3 "x4 abc" "y4 xyz"', "OK")
+ self.assertRunOK("scan", "x1 : y1\nx2 : y2\nx3 : y3\nx4 abc : y4 xyz")
+ self.assertRunFAIL("batchput")
+ self.assertRunFAIL("batchput k1")
+ self.assertRunFAIL("batchput k1 v1 k2")
+
+ def testBlobBatchPut(self):
+ print("Running testBlobBatchPut...")
+
+ dbPath = os.path.join(self.TMP_DIR, self.DB_NAME)
+ self.assertRunOK("batchput x1 y1 --create_if_missing --enable_blob_files", "OK")
+ self.assertRunOK("scan", "x1 : y1")
+ self.assertRunOK(
+ 'batchput --enable_blob_files x2 y2 x3 y3 "x4 abc" "y4 xyz"', "OK"
+ )
+ self.assertRunOK("scan", "x1 : y1\nx2 : y2\nx3 : y3\nx4 abc : y4 xyz")
+
+ blob_files = self.getBlobFiles(dbPath)
+ self.assertTrue(len(blob_files) >= 1)
+
+ def testBlobPut(self):
+ print("Running testBlobPut...")
+
+ dbPath = os.path.join(self.TMP_DIR, self.DB_NAME)
+ self.assertRunOK("put --create_if_missing --enable_blob_files x1 y1", "OK")
+ self.assertRunOK("get x1", "y1")
+ self.assertRunOK("put --enable_blob_files x2 y2", "OK")
+ self.assertRunOK("get x1", "y1")
+ self.assertRunOK("get x2", "y2")
+ self.assertRunFAIL("get x3")
+
+ blob_files = self.getBlobFiles(dbPath)
+ self.assertTrue(len(blob_files) >= 1)
+
+ def testBlobStartingLevel(self):
+ print("Running testBlobStartingLevel...")
+
+ dbPath = os.path.join(self.TMP_DIR, self.DB_NAME)
+ self.assertRunOK(
+ "put --create_if_missing --enable_blob_files --blob_file_starting_level=10 x1 y1",
+ "OK",
+ )
+ self.assertRunOK("get x1", "y1")
+
+ blob_files = self.getBlobFiles(dbPath)
+ self.assertTrue(len(blob_files) == 0)
+
+ self.assertRunOK(
+ "put --enable_blob_files --blob_file_starting_level=0 x2 y2", "OK"
+ )
+ self.assertRunOK("get x1", "y1")
+ self.assertRunOK("get x2", "y2")
+ self.assertRunFAIL("get x3")
+
+ blob_files = self.getBlobFiles(dbPath)
+ self.assertTrue(len(blob_files) >= 1)
+
+ def testCountDelimDump(self):
+ print("Running testCountDelimDump...")
+ self.assertRunOK("batchput x.1 x1 --create_if_missing", "OK")
+ self.assertRunOK("batchput y.abc abc y.2 2 z.13c pqr", "OK")
+ self.assertRunOK(
+ "dump --count_delim",
+ "x => count:1\tsize:5\ny => count:2\tsize:12\nz => count:1\tsize:8",
+ )
+ self.assertRunOK(
+ 'dump --count_delim="."',
+ "x => count:1\tsize:5\ny => count:2\tsize:12\nz => count:1\tsize:8",
+ )
+ self.assertRunOK("batchput x,2 x2 x,abc xabc", "OK")
+ self.assertRunOK(
+ 'dump --count_delim=","',
+ "x => count:2\tsize:14\nx.1 => count:1\tsize:5\ny.2 => count:1\tsize:4\ny.abc => count:1\tsize:8\nz.13c => count:1\tsize:8",
+ )
+
+ def testCountDelimIDump(self):
+ print("Running testCountDelimIDump...")
+ self.assertRunOK("batchput x.1 x1 --create_if_missing", "OK")
+ self.assertRunOK("batchput y.abc abc y.2 2 z.13c pqr", "OK")
+ self.assertRunOK(
+ "idump --count_delim",
+ "x => count:1\tsize:5\ny => count:2\tsize:12\nz => count:1\tsize:8",
+ )
+ self.assertRunOK(
+ 'idump --count_delim="."',
+ "x => count:1\tsize:5\ny => count:2\tsize:12\nz => count:1\tsize:8",
+ )
+ self.assertRunOK("batchput x,2 x2 x,abc xabc", "OK")
+ self.assertRunOK(
+ 'idump --count_delim=","',
+ "x => count:2\tsize:14\nx.1 => count:1\tsize:5\ny.2 => count:1\tsize:4\ny.abc => count:1\tsize:8\nz.13c => count:1\tsize:8",
+ )
+
+ def testInvalidCmdLines(self):
+ print("Running testInvalidCmdLines...")
+ # db not specified
+ self.assertRunFAILFull("put 0x6133 0x6233 --hex --create_if_missing")
+ # No param called he
+ self.assertRunFAIL("put 0x6133 0x6233 --he --create_if_missing")
+ # max_keys is not applicable for put
+ self.assertRunFAIL("put 0x6133 0x6233 --max_keys=1 --create_if_missing")
+ # hex has invalid boolean value
+
+ def testHexPutGet(self):
+ print("Running testHexPutGet...")
+ self.assertRunOK("put a1 b1 --create_if_missing", "OK")
+ self.assertRunOK("scan", "a1 : b1")
+ self.assertRunOK("scan --hex", "0x6131 : 0x6231")
+ self.assertRunFAIL("put --hex 6132 6232")
+ self.assertRunOK("put --hex 0x6132 0x6232", "OK")
+ self.assertRunOK("scan --hex", "0x6131 : 0x6231\n0x6132 : 0x6232")
+ self.assertRunOK("scan", "a1 : b1\na2 : b2")
+ self.assertRunOK("get a1", "b1")
+ self.assertRunOK("get --hex 0x6131", "0x6231")
+ self.assertRunOK("get a2", "b2")
+ self.assertRunOK("get --hex 0x6132", "0x6232")
+ self.assertRunOK("get --key_hex 0x6132", "b2")
+ self.assertRunOK("get --key_hex --value_hex 0x6132", "0x6232")
+ self.assertRunOK("get --value_hex a2", "0x6232")
+ self.assertRunOK(
+ "scan --key_hex --value_hex", "0x6131 : 0x6231\n0x6132 : 0x6232"
+ )
+ self.assertRunOK(
+ "scan --hex --from=0x6131 --to=0x6133", "0x6131 : 0x6231\n0x6132 : 0x6232"
+ )
+ self.assertRunOK("scan --hex --from=0x6131 --to=0x6132", "0x6131 : 0x6231")
+ self.assertRunOK("scan --key_hex", "0x6131 : b1\n0x6132 : b2")
+ self.assertRunOK("scan --value_hex", "a1 : 0x6231\na2 : 0x6232")
+ self.assertRunOK("batchput --hex 0x6133 0x6233 0x6134 0x6234", "OK")
+ self.assertRunOK("scan", "a1 : b1\na2 : b2\na3 : b3\na4 : b4")
+ self.assertRunOK("delete --hex 0x6133", "OK")
+ self.assertRunOK("scan", "a1 : b1\na2 : b2\na4 : b4")
+ self.assertRunOK("checkconsistency", "OK")
+
+ def testTtlPutGet(self):
+ print("Running testTtlPutGet...")
+ self.assertRunOK("put a1 b1 --ttl --create_if_missing", "OK")
+ self.assertRunOK("scan --hex", "0x6131 : 0x6231", True)
+ self.assertRunOK("dump --ttl ", "a1 ==> b1", True)
+ self.assertRunOK("dump --hex --ttl ", "0x6131 ==> 0x6231\nKeys in range: 1")
+ self.assertRunOK("scan --hex --ttl", "0x6131 : 0x6231")
+ self.assertRunOK("get --value_hex a1", "0x6231", True)
+ self.assertRunOK("get --ttl a1", "b1")
+ self.assertRunOK("put a3 b3 --create_if_missing", "OK")
+ # fails because timstamp's length is greater than value's
+ self.assertRunFAIL("get --ttl a3")
+ self.assertRunOK("checkconsistency", "OK")
+
+ def testInvalidCmdLines(self): # noqa: F811 T25377293 Grandfathered in
+ print("Running testInvalidCmdLines...")
+ # db not specified
+ self.assertRunFAILFull("put 0x6133 0x6233 --hex --create_if_missing")
+ # No param called he
+ self.assertRunFAIL("put 0x6133 0x6233 --he --create_if_missing")
+ # max_keys is not applicable for put
+ self.assertRunFAIL("put 0x6133 0x6233 --max_keys=1 --create_if_missing")
+ # hex has invalid boolean value
+ self.assertRunFAIL("put 0x6133 0x6233 --hex=Boo --create_if_missing")
+
+ def testDumpLoad(self):
+ print("Running testDumpLoad...")
+ self.assertRunOK("batchput --create_if_missing x1 y1 x2 y2 x3 y3 x4 y4", "OK")
+ self.assertRunOK("scan", "x1 : y1\nx2 : y2\nx3 : y3\nx4 : y4")
+ origDbPath = os.path.join(self.TMP_DIR, self.DB_NAME)
+
+ # Dump and load without any additional params specified
+ dumpFilePath = os.path.join(self.TMP_DIR, "dump1")
+ loadedDbPath = os.path.join(self.TMP_DIR, "loaded_from_dump1")
+ self.assertTrue(self.dumpDb("--db=%s" % origDbPath, dumpFilePath))
+ self.assertTrue(
+ self.loadDb("--db=%s --create_if_missing" % loadedDbPath, dumpFilePath)
+ )
+ self.assertRunOKFull(
+ "scan --db=%s" % loadedDbPath, "x1 : y1\nx2 : y2\nx3 : y3\nx4 : y4"
+ )
+
+ # Dump and load in hex
+ dumpFilePath = os.path.join(self.TMP_DIR, "dump2")
+ loadedDbPath = os.path.join(self.TMP_DIR, "loaded_from_dump2")
+ self.assertTrue(self.dumpDb("--db=%s --hex" % origDbPath, dumpFilePath))
+ self.assertTrue(
+ self.loadDb(
+ "--db=%s --hex --create_if_missing" % loadedDbPath, dumpFilePath
+ )
+ )
+ self.assertRunOKFull(
+ "scan --db=%s" % loadedDbPath, "x1 : y1\nx2 : y2\nx3 : y3\nx4 : y4"
+ )
+
+ # Dump only a portion of the key range
+ dumpFilePath = os.path.join(self.TMP_DIR, "dump3")
+ loadedDbPath = os.path.join(self.TMP_DIR, "loaded_from_dump3")
+ self.assertTrue(
+ self.dumpDb("--db=%s --from=x1 --to=x3" % origDbPath, dumpFilePath)
+ )
+ self.assertTrue(
+ self.loadDb("--db=%s --create_if_missing" % loadedDbPath, dumpFilePath)
+ )
+ self.assertRunOKFull("scan --db=%s" % loadedDbPath, "x1 : y1\nx2 : y2")
+
+ # Dump upto max_keys rows
+ dumpFilePath = os.path.join(self.TMP_DIR, "dump4")
+ loadedDbPath = os.path.join(self.TMP_DIR, "loaded_from_dump4")
+ self.assertTrue(self.dumpDb("--db=%s --max_keys=3" % origDbPath, dumpFilePath))
+ self.assertTrue(
+ self.loadDb("--db=%s --create_if_missing" % loadedDbPath, dumpFilePath)
+ )
+ self.assertRunOKFull("scan --db=%s" % loadedDbPath, "x1 : y1\nx2 : y2\nx3 : y3")
+
+ # Load into an existing db, create_if_missing is not specified
+ self.assertTrue(self.dumpDb("--db=%s" % origDbPath, dumpFilePath))
+ self.assertTrue(self.loadDb("--db=%s" % loadedDbPath, dumpFilePath))
+ self.assertRunOKFull(
+ "scan --db=%s" % loadedDbPath, "x1 : y1\nx2 : y2\nx3 : y3\nx4 : y4"
+ )
+
+ # Dump and load with WAL disabled
+ dumpFilePath = os.path.join(self.TMP_DIR, "dump5")
+ loadedDbPath = os.path.join(self.TMP_DIR, "loaded_from_dump5")
+ self.assertTrue(self.dumpDb("--db=%s" % origDbPath, dumpFilePath))
+ self.assertTrue(
+ self.loadDb(
+ "--db=%s --disable_wal --create_if_missing" % loadedDbPath, dumpFilePath
+ )
+ )
+ self.assertRunOKFull(
+ "scan --db=%s" % loadedDbPath, "x1 : y1\nx2 : y2\nx3 : y3\nx4 : y4"
+ )
+
+ # Dump and load with lots of extra params specified
+ extraParams = " ".join(
+ [
+ "--bloom_bits=14",
+ "--block_size=1024",
+ "--auto_compaction=true",
+ "--write_buffer_size=4194304",
+ "--file_size=2097152",
+ ]
+ )
+ dumpFilePath = os.path.join(self.TMP_DIR, "dump6")
+ loadedDbPath = os.path.join(self.TMP_DIR, "loaded_from_dump6")
+ self.assertTrue(
+ self.dumpDb("--db=%s %s" % (origDbPath, extraParams), dumpFilePath)
+ )
+ self.assertTrue(
+ self.loadDb(
+ "--db=%s %s --create_if_missing" % (loadedDbPath, extraParams),
+ dumpFilePath,
+ )
+ )
+ self.assertRunOKFull(
+ "scan --db=%s" % loadedDbPath, "x1 : y1\nx2 : y2\nx3 : y3\nx4 : y4"
+ )
+
+ # Dump with count_only
+ dumpFilePath = os.path.join(self.TMP_DIR, "dump7")
+ loadedDbPath = os.path.join(self.TMP_DIR, "loaded_from_dump7")
+ self.assertTrue(self.dumpDb("--db=%s --count_only" % origDbPath, dumpFilePath))
+ self.assertTrue(
+ self.loadDb("--db=%s --create_if_missing" % loadedDbPath, dumpFilePath)
+ )
+ # DB should have atleast one value for scan to work
+ self.assertRunOKFull("put --db=%s k1 v1" % loadedDbPath, "OK")
+ self.assertRunOKFull("scan --db=%s" % loadedDbPath, "k1 : v1")
+
+ # Dump command fails because of typo in params
+ dumpFilePath = os.path.join(self.TMP_DIR, "dump8")
+ self.assertFalse(
+ self.dumpDb("--db=%s --create_if_missing" % origDbPath, dumpFilePath)
+ )
+
+ # Dump and load with BlobDB enabled
+ blobParams = " ".join(
+ ["--enable_blob_files", "--min_blob_size=1", "--blob_file_size=2097152"]
+ )
+ dumpFilePath = os.path.join(self.TMP_DIR, "dump9")
+ loadedDbPath = os.path.join(self.TMP_DIR, "loaded_from_dump9")
+ self.assertTrue(self.dumpDb("--db=%s" % (origDbPath), dumpFilePath))
+ self.assertTrue(
+ self.loadDb(
+ "--db=%s %s --create_if_missing --disable_wal"
+ % (loadedDbPath, blobParams),
+ dumpFilePath,
+ )
+ )
+ self.assertRunOKFull(
+ "scan --db=%s" % loadedDbPath, "x1 : y1\nx2 : y2\nx3 : y3\nx4 : y4"
+ )
+ blob_files = self.getBlobFiles(loadedDbPath)
+ self.assertTrue(len(blob_files) >= 1)
+
+ def testIDumpBasics(self):
+ print("Running testIDumpBasics...")
+ self.assertRunOK("put a val --create_if_missing", "OK")
+ self.assertRunOK("put b val", "OK")
+ self.assertRunOK(
+ "idump",
+ "'a' seq:1, type:1 => val\n"
+ "'b' seq:2, type:1 => val\nInternal keys in range: 2",
+ )
+ self.assertRunOK(
+ "idump --input_key_hex --from=%s --to=%s" % (hex(ord("a")), hex(ord("b"))),
+ "'a' seq:1, type:1 => val\nInternal keys in range: 1",
+ )
+
+ def testIDumpDecodeBlobIndex(self):
+ print("Running testIDumpDecodeBlobIndex...")
+ self.assertRunOK("put a val --create_if_missing", "OK")
+ self.assertRunOK("put b val --enable_blob_files", "OK")
+
+ # Pattern to expect from dump with decode_blob_index flag enabled.
+ regex = ".*\[blob ref\].*"
+ expected_pattern = re.compile(regex)
+ cmd = "idump %s --decode_blob_index"
+ self.assertRunOKFull(
+ (cmd) % (self.dbParam(self.DB_NAME)),
+ expected_pattern,
+ unexpected=False,
+ isPattern=True,
+ )
+
+ def testMiscAdminTask(self):
+ print("Running testMiscAdminTask...")
+ # These tests need to be improved; for example with asserts about
+ # whether compaction or level reduction actually took place.
+ self.assertRunOK("batchput --create_if_missing x1 y1 x2 y2 x3 y3 x4 y4", "OK")
+ self.assertRunOK("scan", "x1 : y1\nx2 : y2\nx3 : y3\nx4 : y4")
+ origDbPath = os.path.join(self.TMP_DIR, self.DB_NAME)
+
+ self.assertTrue(0 == run_err_null("./ldb compact --db=%s" % origDbPath))
+ self.assertRunOK("scan", "x1 : y1\nx2 : y2\nx3 : y3\nx4 : y4")
+
+ self.assertTrue(
+ 0 == run_err_null("./ldb reduce_levels --db=%s --new_levels=2" % origDbPath)
+ )
+ self.assertRunOK("scan", "x1 : y1\nx2 : y2\nx3 : y3\nx4 : y4")
+
+ self.assertTrue(
+ 0 == run_err_null("./ldb reduce_levels --db=%s --new_levels=3" % origDbPath)
+ )
+ self.assertRunOK("scan", "x1 : y1\nx2 : y2\nx3 : y3\nx4 : y4")
+
+ self.assertTrue(
+ 0 == run_err_null("./ldb compact --db=%s --from=x1 --to=x3" % origDbPath)
+ )
+ self.assertRunOK("scan", "x1 : y1\nx2 : y2\nx3 : y3\nx4 : y4")
+
+ self.assertTrue(
+ 0
+ == run_err_null(
+ "./ldb compact --db=%s --hex --from=0x6131 --to=0x6134" % origDbPath
+ )
+ )
+ self.assertRunOK("scan", "x1 : y1\nx2 : y2\nx3 : y3\nx4 : y4")
+
+ # TODO(dilip): Not sure what should be passed to WAL.Currently corrupted.
+ self.assertTrue(
+ 0
+ == run_err_null(
+ "./ldb dump_wal --db=%s --walfile=%s --header"
+ % (origDbPath, os.path.join(origDbPath, "LOG"))
+ )
+ )
+ self.assertRunOK("scan", "x1 : y1\nx2 : y2\nx3 : y3\nx4 : y4")
+
+ def testCheckConsistency(self):
+ print("Running testCheckConsistency...")
+
+ dbPath = os.path.join(self.TMP_DIR, self.DB_NAME)
+ self.assertRunOK("put x1 y1 --create_if_missing", "OK")
+ self.assertRunOK("put x2 y2", "OK")
+ self.assertRunOK("get x1", "y1")
+ self.assertRunOK("checkconsistency", "OK")
+
+ sstFilePath = my_check_output(
+ "ls %s" % os.path.join(dbPath, "*.sst"), shell=True
+ )
+
+ # Modify the file
+ my_check_output("echo 'evil' > %s" % sstFilePath, shell=True)
+ self.assertRunFAIL("checkconsistency")
+
+ # Delete the file
+ my_check_output("rm -f %s" % sstFilePath, shell=True)
+ self.assertRunFAIL("checkconsistency")
+
+ def dumpLiveFiles(self, params, dumpFile):
+ return 0 == run_err_null("./ldb dump_live_files %s > %s" % (params, dumpFile))
+
+ def testDumpLiveFiles(self):
+ print("Running testDumpLiveFiles...")
+
+ dbPath = os.path.join(self.TMP_DIR, self.DB_NAME)
+ self.assertRunOK("put x1 y1 --create_if_missing", "OK")
+ self.assertRunOK("put x2 y2 --enable_blob_files", "OK")
+ dumpFilePath = os.path.join(self.TMP_DIR, "dump1")
+ self.assertTrue(self.dumpLiveFiles("--db=%s" % dbPath, dumpFilePath))
+ self.assertRunOK("delete x1", "OK")
+ self.assertRunOK("put x3 y3", "OK")
+ dumpFilePath = os.path.join(self.TMP_DIR, "dump2")
+
+ # Test that if the user provides a db path that ends with
+ # a slash '/', there is no double (or more!) slashes in the
+ # SST and manifest file names.
+
+ # Add a '/' at the end of dbPath (which normally shouldnt contain any)
+ if dbPath[-1] != "/":
+ dbPath += "/"
+
+ # Call the dump_live_files function with the edited dbPath name.
+ self.assertTrue(
+ self.dumpLiveFiles(
+ "--db=%s --decode_blob_index --dump_uncompressed_blobs" % dbPath,
+ dumpFilePath,
+ )
+ )
+
+ # Investigate the output
+ with open(dumpFilePath, "r") as tmp:
+ data = tmp.read()
+
+ # Check that all the SST filenames have a correct full path (no multiple '/').
+ sstFileList = re.findall(r"%s.*\d+.sst" % dbPath, data)
+ self.assertTrue(len(sstFileList) >= 1)
+ for sstFilename in sstFileList:
+ filenumber = re.findall(r"\d+.sst", sstFilename)[0]
+ self.assertEqual(sstFilename, dbPath + filenumber)
+
+ # Check that all the Blob filenames have a correct full path (no multiple '/').
+ blobFileList = re.findall(r"%s.*\d+.blob" % dbPath, data)
+ self.assertTrue(len(blobFileList) >= 1)
+ for blobFilename in blobFileList:
+ filenumber = re.findall(r"\d+.blob", blobFilename)[0]
+ self.assertEqual(blobFilename, dbPath + filenumber)
+
+ # Check that all the manifest filenames
+ # have a correct full path (no multiple '/').
+ manifestFileList = re.findall(r"%s.*MANIFEST-\d+" % dbPath, data)
+ self.assertTrue(len(manifestFileList) >= 1)
+ for manifestFilename in manifestFileList:
+ filenumber = re.findall(r"(?<=MANIFEST-)\d+", manifestFilename)[0]
+ self.assertEqual(manifestFilename, dbPath + "MANIFEST-" + filenumber)
+
+ # Check that the blob file index is decoded.
+ decodedBlobIndex = re.findall(r"\[blob ref\]", data)
+ self.assertTrue(len(decodedBlobIndex) >= 1)
+
+ def listLiveFilesMetadata(self, params, dumpFile):
+ return 0 == run_err_null(
+ "./ldb list_live_files_metadata %s > %s" % (params, dumpFile)
+ )
+
+ def testListLiveFilesMetadata(self):
+ print("Running testListLiveFilesMetadata...")
+
+ dbPath = os.path.join(self.TMP_DIR, self.DB_NAME)
+ self.assertRunOK("put x1 y1 --create_if_missing", "OK")
+ self.assertRunOK("put x2 y2", "OK")
+
+ # Compare the SST filename and the level of list_live_files_metadata
+ # with the data collected from dump_live_files.
+ dumpFilePath1 = os.path.join(self.TMP_DIR, "dump1")
+ self.assertTrue(self.dumpLiveFiles("--db=%s" % dbPath, dumpFilePath1))
+ dumpFilePath2 = os.path.join(self.TMP_DIR, "dump2")
+ self.assertTrue(
+ self.listLiveFilesMetadata(
+ "--sort_by_filename --db=%s" % dbPath, dumpFilePath2
+ )
+ )
+
+ # Collect SST filename and level from dump_live_files
+ with open(dumpFilePath1, "r") as tmp:
+ data = tmp.read()
+ filename1 = re.findall(r".*\d+\.sst", data)[0]
+ level1 = re.findall(r"level:\d+", data)[0].split(":")[1]
+
+ # Collect SST filename and level from list_live_files_metadata
+ with open(dumpFilePath2, "r") as tmp:
+ data = tmp.read()
+ filename2 = re.findall(r".*\d+\.sst", data)[0]
+ level2 = re.findall(r"level \d+", data)[0].split(" ")[1]
+
+ # Assert equality between filenames and levels.
+ self.assertEqual(filename1, filename2)
+ self.assertEqual(level1, level2)
+
+ # Create multiple column families and compare the output
+ # of list_live_files_metadata with dump_live_files once again.
+ # Create new CF, and insert data:
+ self.assertRunOK("create_column_family mycol1", "OK")
+ self.assertRunOK("put --column_family=mycol1 v1 v2", "OK")
+ self.assertRunOK("create_column_family mycol2", "OK")
+ self.assertRunOK("put --column_family=mycol2 h1 h2", "OK")
+ self.assertRunOK("put --column_family=mycol2 h3 h4", "OK")
+
+ # Call dump_live_files and list_live_files_metadata
+ # and pipe the output to compare them later.
+ dumpFilePath3 = os.path.join(self.TMP_DIR, "dump3")
+ self.assertTrue(self.dumpLiveFiles("--db=%s" % dbPath, dumpFilePath3))
+ dumpFilePath4 = os.path.join(self.TMP_DIR, "dump4")
+ self.assertTrue(
+ self.listLiveFilesMetadata(
+ "--sort_by_filename --db=%s" % dbPath, dumpFilePath4
+ )
+ )
+
+ # dump_live_files:
+ # parse the output and create a map:
+ # [key: sstFilename]->[value:[LSM level, Column Family Name]]
+ referenceMap = {}
+ with open(dumpFilePath3, "r") as tmp:
+ data = tmp.read()
+ # Note: the following regex are contingent on what the
+ # dump_live_files outputs.
+ namesAndLevels = re.findall(r"\d+.sst level:\d+", data)
+ cfs = re.findall(r"(?<=column family name=)\w+", data)
+ # re.findall should not reorder the data.
+ # Therefore namesAndLevels[i] matches the data from cfs[i].
+ for count, nameAndLevel in enumerate(namesAndLevels):
+ sstFilename = re.findall(r"\d+.sst", nameAndLevel)[0]
+ sstLevel = re.findall(r"(?<=level:)\d+", nameAndLevel)[0]
+ cf = cfs[count]
+ referenceMap[sstFilename] = [sstLevel, cf]
+
+ # list_live_files_metadata:
+ # parse the output and create a map:
+ # [key: sstFilename]->[value:[LSM level, Column Family Name]]
+ testMap = {}
+ with open(dumpFilePath4, "r") as tmp:
+ data = tmp.read()
+ # Since for each SST file, all the information is contained
+ # on one line, the parsing is easy to perform and relies on
+ # the appearance of an "00xxx.sst" pattern.
+ sstLines = re.findall(r".*\d+.sst.*", data)
+ for line in sstLines:
+ sstFilename = re.findall(r"\d+.sst", line)[0]
+ sstLevel = re.findall(r"(?<=level )\d+", line)[0]
+ cf = re.findall(r"(?<=column family \')\w+(?=\')", line)[0]
+ testMap[sstFilename] = [sstLevel, cf]
+
+ # Compare the map obtained from dump_live_files and the map
+ # obtained from list_live_files_metadata. Everything should match.
+ self.assertEqual(referenceMap, testMap)
+
+ def getManifests(self, directory):
+ return glob.glob(directory + "/MANIFEST-*")
+
+ def getSSTFiles(self, directory):
+ return glob.glob(directory + "/*.sst")
+
+ def getWALFiles(self, directory):
+ return glob.glob(directory + "/*.log")
+
+ def getBlobFiles(self, directory):
+ return glob.glob(directory + "/*.blob")
+
+ def copyManifests(self, src, dest):
+ return 0 == run_err_null("cp " + src + " " + dest)
+
+ def testManifestDump(self):
+ print("Running testManifestDump...")
+ dbPath = os.path.join(self.TMP_DIR, self.DB_NAME)
+ self.assertRunOK("put 1 1 --create_if_missing", "OK")
+ self.assertRunOK("put 2 2", "OK")
+ self.assertRunOK("put 3 3", "OK")
+ # Pattern to expect from manifest_dump.
+ num = "[0-9]+"
+ st = ".*"
+ subpat = st + " seq:" + num + ", type:" + num
+ regex = num + ":" + num + "\[" + subpat + ".." + subpat + "\]"
+ expected_pattern = re.compile(regex)
+ cmd = "manifest_dump --db=%s"
+ manifest_files = self.getManifests(dbPath)
+ self.assertTrue(len(manifest_files) == 1)
+ # Test with the default manifest file in dbPath.
+ self.assertRunOKFull(
+ cmd % dbPath, expected_pattern, unexpected=False, isPattern=True
+ )
+ self.copyManifests(manifest_files[0], manifest_files[0] + "1")
+ manifest_files = self.getManifests(dbPath)
+ self.assertTrue(len(manifest_files) == 2)
+ # Test with multiple manifest files in dbPath.
+ self.assertRunFAILFull(cmd % dbPath)
+ # Running it with the copy we just created should pass.
+ self.assertRunOKFull(
+ (cmd + " --path=%s") % (dbPath, manifest_files[1]),
+ expected_pattern,
+ unexpected=False,
+ isPattern=True,
+ )
+ # Make sure that using the dump with --path will result in identical
+ # output as just using manifest_dump.
+ cmd = "dump --path=%s"
+ self.assertRunOKFull(
+ (cmd) % (manifest_files[1]),
+ expected_pattern,
+ unexpected=False,
+ isPattern=True,
+ )
+
+ # Check if null characters doesn't infer with output format.
+ self.assertRunOK("put a1 b1", "OK")
+ self.assertRunOK("put a2 b2", "OK")
+ self.assertRunOK("put --hex 0x12000DA0 0x80C0000B", "OK")
+ self.assertRunOK("put --hex 0x7200004f 0x80000004", "OK")
+ self.assertRunOK("put --hex 0xa000000a 0xf000000f", "OK")
+ self.assertRunOK("put a3 b3", "OK")
+ self.assertRunOK("put a4 b4", "OK")
+
+ # Verifies that all "levels" are printed out.
+ # There should be 66 mentions of levels.
+ expected_verbose_output = re.compile("matched")
+ # Test manifest_dump verbose and verify that key 0x7200004f
+ # is present. Note that we are forced to use grep here because
+ # an output with a non-terminating null character in it isn't piped
+ # correctly through the Python subprocess object.
+ # Also note that 0x72=r and 0x4f=O, hence the regex \'r.{2}O\'
+ # (we cannot use null character in the subprocess input either,
+ # so we have to use '.{2}')
+ cmd_verbose = (
+ "manifest_dump --verbose --db=%s | grep -aq $''r.{2}O'' && echo 'matched' || echo 'not matched'"
+ % dbPath
+ )
+
+ self.assertRunOKFull(
+ cmd_verbose, expected_verbose_output, unexpected=False, isPattern=True
+ )
+
+ def testGetProperty(self):
+ print("Running testGetProperty...")
+ dbPath = os.path.join(self.TMP_DIR, self.DB_NAME)
+ self.assertRunOK("put 1 1 --create_if_missing", "OK")
+ self.assertRunOK("put 2 2", "OK")
+ # A "string" property
+ cmd = "--db=%s get_property rocksdb.estimate-num-keys"
+ self.assertRunOKFull(cmd % dbPath, "rocksdb.estimate-num-keys: 2")
+ # A "map" property
+ # FIXME: why doesn't this pick up two entries?
+ cmd = "--db=%s get_property rocksdb.aggregated-table-properties"
+ part = "rocksdb.aggregated-table-properties.num_entries: "
+ expected_pattern = re.compile(part)
+ self.assertRunOKFull(
+ cmd % dbPath, expected_pattern, unexpected=False, isPattern=True
+ )
+ # An invalid property
+ cmd = "--db=%s get_property rocksdb.this-property-does-not-exist"
+ self.assertRunFAILFull(cmd % dbPath)
+
+ def testSSTDump(self):
+ print("Running testSSTDump...")
+
+ dbPath = os.path.join(self.TMP_DIR, self.DB_NAME)
+ self.assertRunOK("put sst1 sst1_val --create_if_missing", "OK")
+ self.assertRunOK("put sst2 sst2_val --enable_blob_files", "OK")
+ self.assertRunOK("get sst1", "sst1_val")
+
+ # Pattern to expect from SST dump.
+ regex = ".*Sst file format:.*\n.*\[blob ref\].*"
+ expected_pattern = re.compile(regex)
+
+ sst_files = self.getSSTFiles(dbPath)
+ self.assertTrue(len(sst_files) >= 1)
+ cmd = "dump --path=%s --decode_blob_index"
+ self.assertRunOKFull(
+ (cmd) % (sst_files[0]), expected_pattern, unexpected=False, isPattern=True
+ )
+
+ def testBlobDump(self):
+ print("Running testBlobDump")
+ dbPath = os.path.join(self.TMP_DIR, self.DB_NAME)
+ self.assertRunOK("batchput x1 y1 --create_if_missing --enable_blob_files", "OK")
+ self.assertRunOK(
+ 'batchput --enable_blob_files x2 y2 x3 y3 "x4 abc" "y4 xyz"', "OK"
+ )
+
+ # Pattern to expect from blob file dump.
+ regex = ".*Blob log header[\s\S]*Blob log footer[\s\S]*Read record[\s\S]*Summary" # noqa
+ expected_pattern = re.compile(regex)
+ blob_files = self.getBlobFiles(dbPath)
+ self.assertTrue(len(blob_files) >= 1)
+ cmd = "dump --path=%s --dump_uncompressed_blobs"
+ self.assertRunOKFull(
+ (cmd) % (blob_files[0]), expected_pattern, unexpected=False, isPattern=True
+ )
+
+ def testWALDump(self):
+ print("Running testWALDump...")
+
+ dbPath = os.path.join(self.TMP_DIR, self.DB_NAME)
+ self.assertRunOK("put wal1 wal1_val --create_if_missing", "OK")
+ self.assertRunOK("put wal2 wal2_val", "OK")
+ self.assertRunOK("get wal1", "wal1_val")
+
+ # Pattern to expect from WAL dump.
+ regex = "^Sequence,Count,ByteSize,Physical Offset,Key\(s\).*"
+ expected_pattern = re.compile(regex)
+
+ wal_files = self.getWALFiles(dbPath)
+ self.assertTrue(len(wal_files) >= 1)
+ cmd = "dump --path=%s"
+ self.assertRunOKFull(
+ (cmd) % (wal_files[0]), expected_pattern, unexpected=False, isPattern=True
+ )
+
+ def testListColumnFamilies(self):
+ print("Running testListColumnFamilies...")
+ self.assertRunOK("put x1 y1 --create_if_missing", "OK")
+ cmd = 'list_column_families | grep -v "Column families"'
+ # Test on valid dbPath.
+ self.assertRunOK(cmd, "{default}")
+ # Test on empty path.
+ self.assertRunFAIL(cmd)
+
+ def testColumnFamilies(self):
+ print("Running testColumnFamilies...")
+ _ = os.path.join(self.TMP_DIR, self.DB_NAME)
+ self.assertRunOK("put cf1_1 1 --create_if_missing", "OK")
+ self.assertRunOK("put cf1_2 2 --create_if_missing", "OK")
+ self.assertRunOK("put cf1_3 3 --try_load_options", "OK")
+ # Given non-default column family to single CF DB.
+ self.assertRunFAIL("get cf1_1 --column_family=two")
+ self.assertRunOK("create_column_family two", "OK")
+ self.assertRunOK("put cf2_1 1 --create_if_missing --column_family=two", "OK")
+ self.assertRunOK("put cf2_2 2 --create_if_missing --column_family=two", "OK")
+ self.assertRunOK("delete cf1_2", "OK")
+ self.assertRunOK("create_column_family three", "OK")
+ self.assertRunOK("delete cf2_2 --column_family=two", "OK")
+ self.assertRunOK("put cf3_1 3 --create_if_missing --column_family=three", "OK")
+ self.assertRunOK("get cf1_1 --column_family=default", "1")
+ self.assertRunOK("dump --column_family=two", "cf2_1 ==> 1\nKeys in range: 1")
+ self.assertRunOK(
+ "dump --column_family=two --try_load_options",
+ "cf2_1 ==> 1\nKeys in range: 1",
+ )
+ self.assertRunOK("dump", "cf1_1 ==> 1\ncf1_3 ==> 3\nKeys in range: 2")
+ self.assertRunOK("get cf2_1 --column_family=two", "1")
+ self.assertRunOK("get cf3_1 --column_family=three", "3")
+ self.assertRunOK("drop_column_family three", "OK")
+ # non-existing column family.
+ self.assertRunFAIL("get cf3_1 --column_family=four")
+ self.assertRunFAIL("drop_column_family four")
+
+ def testIngestExternalSst(self):
+ print("Running testIngestExternalSst...")
+
+ # Dump, load, write external sst and ingest it in another db
+ dbPath = os.path.join(self.TMP_DIR, "db1")
+ self.assertRunOK(
+ "batchput --db=%s --create_if_missing x1 y1 x2 y2 x3 y3 x4 y4" % dbPath,
+ "OK",
+ )
+ self.assertRunOK("scan --db=%s" % dbPath, "x1 : y1\nx2 : y2\nx3 : y3\nx4 : y4")
+ dumpFilePath = os.path.join(self.TMP_DIR, "dump1")
+ with open(dumpFilePath, "w") as f:
+ f.write("x1 ==> y10\nx2 ==> y20\nx3 ==> y30\nx4 ==> y40")
+ externSstPath = os.path.join(self.TMP_DIR, "extern_data1.sst")
+ self.assertTrue(
+ self.writeExternSst(
+ "--create_if_missing --db=%s" % dbPath, dumpFilePath, externSstPath
+ )
+ )
+ # cannot ingest if allow_global_seqno is false
+ self.assertFalse(
+ self.ingestExternSst(
+ "--create_if_missing --allow_global_seqno=false --db=%s" % dbPath,
+ externSstPath,
+ )
+ )
+ self.assertTrue(
+ self.ingestExternSst(
+ "--create_if_missing --allow_global_seqno --db=%s" % dbPath,
+ externSstPath,
+ )
+ )
+ self.assertRunOKFull(
+ "scan --db=%s" % dbPath, "x1 : y10\nx2 : y20\nx3 : y30\nx4 : y40"
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/src/rocksdb/tools/ldb_tool.cc b/src/rocksdb/tools/ldb_tool.cc
new file mode 100644
index 000000000..eadb6a095
--- /dev/null
+++ b/src/rocksdb/tools/ldb_tool.cc
@@ -0,0 +1,184 @@
+// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+// This source code is licensed under both the GPLv2 (found in the
+// COPYING file in the root directory) and Apache 2.0 License
+// (found in the LICENSE.Apache file in the root directory).
+//
+#ifndef ROCKSDB_LITE
+#include "rocksdb/ldb_tool.h"
+
+#include "rocksdb/utilities/ldb_cmd.h"
+#include "tools/ldb_cmd_impl.h"
+
+namespace ROCKSDB_NAMESPACE {
+
+LDBOptions::LDBOptions() {}
+
+void LDBCommandRunner::PrintHelp(const LDBOptions& ldb_options,
+ const char* /*exec_name*/, bool to_stderr) {
+ std::string ret;
+
+ ret.append(ldb_options.print_help_header);
+ ret.append("\n\n");
+ ret.append("commands MUST specify --" + LDBCommand::ARG_DB +
+ "=<full_path_to_db_directory> when necessary\n");
+ ret.append("\n");
+ ret.append("commands can optionally specify\n");
+ ret.append(" --" + LDBCommand::ARG_ENV_URI + "=<uri_of_environment> or --" +
+ LDBCommand::ARG_FS_URI + "=<uri_of_filesystem> if necessary");
+ ret.append("\n");
+ ret.append(" --" + LDBCommand::ARG_SECONDARY_PATH +
+ "=<secondary_path> to open DB as secondary instance. Operations "
+ "not supported in secondary instance will fail.\n\n");
+ ret.append(
+ "The following optional parameters control if keys/values are "
+ "input/output as hex or as plain strings:\n");
+ ret.append(" --" + LDBCommand::ARG_KEY_HEX +
+ " : Keys are input/output as hex\n");
+ ret.append(" --" + LDBCommand::ARG_VALUE_HEX +
+ " : Values are input/output as hex\n");
+ ret.append(" --" + LDBCommand::ARG_HEX +
+ " : Both keys and values are input/output as hex\n");
+ ret.append("\n");
+
+ ret.append(
+ "The following optional parameters control the database "
+ "internals:\n");
+ ret.append(
+ " --" + LDBCommand::ARG_CF_NAME +
+ "=<string> : name of the column family to operate on. default: default "
+ "column family\n");
+ ret.append(" --" + LDBCommand::ARG_TTL +
+ " with 'put','get','scan','dump','query','batchput'"
+ " : DB supports ttl and value is internally timestamp-suffixed\n");
+ ret.append(" --" + LDBCommand::ARG_TRY_LOAD_OPTIONS +
+ " : Try to load option file from DB. Default to true if " +
+ LDBCommand::ARG_DB +
+ " is specified and not creating a new DB and not open as TTL DB. "
+ "Can be set to false explicitly.\n");
+ ret.append(" --" + LDBCommand::ARG_DISABLE_CONSISTENCY_CHECKS +
+ " : Set options.force_consistency_checks = false.\n");
+ ret.append(" --" + LDBCommand::ARG_IGNORE_UNKNOWN_OPTIONS +
+ " : Ignore unknown options when loading option file.\n");
+ ret.append(" --" + LDBCommand::ARG_BLOOM_BITS + "=<int,e.g.:14>\n");
+ ret.append(" --" + LDBCommand::ARG_FIX_PREFIX_LEN + "=<int,e.g.:14>\n");
+ ret.append(" --" + LDBCommand::ARG_COMPRESSION_TYPE +
+ "=<no|snappy|zlib|bzip2|lz4|lz4hc|xpress|zstd>\n");
+ ret.append(" --" + LDBCommand::ARG_COMPRESSION_MAX_DICT_BYTES +
+ "=<int,e.g.:16384>\n");
+ ret.append(" --" + LDBCommand::ARG_BLOCK_SIZE + "=<block_size_in_bytes>\n");
+ ret.append(" --" + LDBCommand::ARG_AUTO_COMPACTION + "=<true|false>\n");
+ ret.append(" --" + LDBCommand::ARG_DB_WRITE_BUFFER_SIZE +
+ "=<int,e.g.:16777216>\n");
+ ret.append(" --" + LDBCommand::ARG_WRITE_BUFFER_SIZE +
+ "=<int,e.g.:4194304>\n");
+ ret.append(" --" + LDBCommand::ARG_FILE_SIZE + "=<int,e.g.:2097152>\n");
+ ret.append(" --" + LDBCommand::ARG_ENABLE_BLOB_FILES +
+ " : Enable key-value separation using BlobDB\n");
+ ret.append(" --" + LDBCommand::ARG_MIN_BLOB_SIZE + "=<int,e.g.:2097152>\n");
+ ret.append(" --" + LDBCommand::ARG_BLOB_FILE_SIZE + "=<int,e.g.:2097152>\n");
+ ret.append(" --" + LDBCommand::ARG_BLOB_COMPRESSION_TYPE +
+ "=<no|snappy|zlib|bzip2|lz4|lz4hc|xpress|zstd>\n");
+ ret.append(" --" + LDBCommand::ARG_ENABLE_BLOB_GARBAGE_COLLECTION +
+ " : Enable blob garbage collection\n");
+ ret.append(" --" + LDBCommand::ARG_BLOB_GARBAGE_COLLECTION_AGE_CUTOFF +
+ "=<double,e.g.:0.25>\n");
+ ret.append(" --" + LDBCommand::ARG_BLOB_GARBAGE_COLLECTION_FORCE_THRESHOLD +
+ "=<double,e.g.:0.25>\n");
+ ret.append(" --" + LDBCommand::ARG_BLOB_COMPACTION_READAHEAD_SIZE +
+ "=<int,e.g.:2097152>\n");
+
+ ret.append("\n\n");
+ ret.append("Data Access Commands:\n");
+ PutCommand::Help(ret);
+ GetCommand::Help(ret);
+ BatchPutCommand::Help(ret);
+ ScanCommand::Help(ret);
+ DeleteCommand::Help(ret);
+ DeleteRangeCommand::Help(ret);
+ DBQuerierCommand::Help(ret);
+ ApproxSizeCommand::Help(ret);
+ CheckConsistencyCommand::Help(ret);
+ ListFileRangeDeletesCommand::Help(ret);
+
+ ret.append("\n\n");
+ ret.append("Admin Commands:\n");
+ WALDumperCommand::Help(ret);
+ CompactorCommand::Help(ret);
+ ReduceDBLevelsCommand::Help(ret);
+ ChangeCompactionStyleCommand::Help(ret);
+ DBDumperCommand::Help(ret);
+ DBLoaderCommand::Help(ret);
+ ManifestDumpCommand::Help(ret);
+ UpdateManifestCommand::Help(ret);
+ FileChecksumDumpCommand::Help(ret);
+ GetPropertyCommand::Help(ret);
+ ListColumnFamiliesCommand::Help(ret);
+ CreateColumnFamilyCommand::Help(ret);
+ DropColumnFamilyCommand::Help(ret);
+ DBFileDumperCommand::Help(ret);
+ InternalDumpCommand::Help(ret);
+ DBLiveFilesMetadataDumperCommand::Help(ret);
+ RepairCommand::Help(ret);
+ BackupCommand::Help(ret);
+ RestoreCommand::Help(ret);
+ CheckPointCommand::Help(ret);
+ WriteExternalSstFilesCommand::Help(ret);
+ IngestExternalSstFilesCommand::Help(ret);
+ UnsafeRemoveSstFileCommand::Help(ret);
+
+ fprintf(to_stderr ? stderr : stdout, "%s\n", ret.c_str());
+}
+
+int LDBCommandRunner::RunCommand(
+ int argc, char const* const* argv, Options options,
+ const LDBOptions& ldb_options,
+ const std::vector<ColumnFamilyDescriptor>* column_families) {
+ if (argc <= 2) {
+ if (argc <= 1) {
+ PrintHelp(ldb_options, argv[0], /*to_stderr*/ true);
+ return 1;
+ } else if (std::string(argv[1]) == "--version") {
+ printf("ldb from RocksDB %d.%d.%d\n", ROCKSDB_MAJOR, ROCKSDB_MINOR,
+ ROCKSDB_PATCH);
+ return 0;
+ } else if (std::string(argv[1]) == "--help") {
+ PrintHelp(ldb_options, argv[0], /*to_stderr*/ false);
+ return 0;
+ } else {
+ PrintHelp(ldb_options, argv[0], /*to_stderr*/ true);
+ return 1;
+ }
+ }
+
+ LDBCommand* cmdObj = LDBCommand::InitFromCmdLineArgs(
+ argc, argv, options, ldb_options, column_families);
+ if (cmdObj == nullptr) {
+ fprintf(stderr, "Unknown command\n");
+ PrintHelp(ldb_options, argv[0], /*to_stderr*/ true);
+ return 1;
+ }
+
+ if (!cmdObj->ValidateCmdLineOptions()) {
+ return 1;
+ }
+
+ cmdObj->Run();
+ LDBCommandExecuteResult ret = cmdObj->GetExecuteState();
+ if (!ret.ToString().empty()) {
+ fprintf(stderr, "%s\n", ret.ToString().c_str());
+ }
+ delete cmdObj;
+
+ return ret.IsFailed() ? 1 : 0;
+}
+
+void LDBTool::Run(int argc, char** argv, Options options,
+ const LDBOptions& ldb_options,
+ const std::vector<ColumnFamilyDescriptor>* column_families) {
+ int error_code = LDBCommandRunner::RunCommand(argc, argv, options,
+ ldb_options, column_families);
+ exit(error_code);
+}
+} // namespace ROCKSDB_NAMESPACE
+
+#endif // ROCKSDB_LITE
diff --git a/src/rocksdb/tools/pflag b/src/rocksdb/tools/pflag
new file mode 100755
index 000000000..f3394a666
--- /dev/null
+++ b/src/rocksdb/tools/pflag
@@ -0,0 +1,217 @@
+#!/usr/bin/env bash
+#
+#(c) 2004-present, Facebook, all rights reserved.
+# See the LICENSE file for usage and distribution rights.
+#
+
+trap 'echo "Caught exception, dying"; exit' 1 2 3 15
+
+ME=`basename $0`
+SERVER=`hostname`
+
+#parameters used
+#
+Dump_Config=0
+DEBUG=
+OS=`/bin/uname -s`
+VMEM=
+RSS=
+CPU=
+VERBOSE=
+VAR=
+LIMIT=
+ACTION=
+N=
+WAIT=
+
+#
+#supported OS: Linux only for now. Easy to add
+#
+oscheck() {
+ case ${OS} in
+ Linux)
+ VMEM=vsz
+ RSS=rss
+ CPU=bsdtime
+ ;;
+ *)
+ die "Unsupported OS ${OS}. Send a bug report with OS you need supported. Thanks."
+ ;;
+ esac
+}
+
+
+verbose() {
+ if [ "x$DEBUG" != "x" ]; then
+ echo "$@" >&2
+ fi
+}
+
+warn() {
+ echo "$@" >&2
+}
+
+die() {
+ echo "ERROR: " "$@" >&2;
+ exit;
+}
+
+dump_config() {
+ cat <<EOCONFIG;
+$ME running on ${HOSTNAME} at `date`
+
+Configuration for this run:
+ PID to monitor : ${PID}
+ Resource monitored : ${VAR}
+ Resource limit : ${LIMIT}
+ Check every : ${WAIT} seconds
+ No. of times run : ${N}
+ What to do : ${ACTION}
+EOCONFIG
+
+}
+
+usage() {
+ cat <<USAGE; exit
+$@
+
+Usage ${ME} -p pid [-x {VMEM|RSS|CPU}] -l limit [-a {warn|die|kill}] [-n cycles] [-w wait]
+
+Monitor a process for set of violations. Options:
+
+ -p: PID of process to monitor
+
+ -x: metric to sense. Currently only VMEM/RSS/CPU are supported. Defaults to VMEM
+
+ -l: what is the threshold/limit for the metric that is being sensed.
+ Examples: "-l 100m", "-l 1.5g" (for VMEM/RSS), "-l 5:04" 5:04 in BSDTIME for CPU
+ NOTE: defaults to 1GB
+
+ -a: action. Currently {warn|die|kill} are supported.
+ The default action is to 'warn'. Here is the behavior:
+
+ warn: complain if usage exceeds threshold, but continue monitoring
+ kill: complain, kill the db_bench process and exit
+ die: if usage exceeds threshold, die immediately
+
+ -n: number of cycles to monitor. Default is to monitor until PID no longer exists.
+
+ -w: wait time per cycle of monitoring. Default is 5 seconds.
+
+ -v: verbose messaging
+
+USAGE
+
+}
+
+#set default values if none given
+set_defaults_if_noopt_given() {
+
+ : ${VAR:=vsz}
+ : ${LIMIT:=1024000}
+ : ${WAIT:=5}
+ : ${N:=999999}
+ : ${ACTION:=warn}
+}
+
+validate_options() {
+ if [ "x$PID" = "x" -a $Dump_Config -ne 1 ]; then
+ usage "PID is mandatory"
+ fi
+}
+
+###### START
+
+
+ while getopts ":p:x:l:a:n:t:vhd" opt; do
+ case $opt in
+ d)
+ Dump_Config=1
+ ;;
+ h)
+ usage;
+ ;;
+ a)
+ ACTION=${OPTARG};
+ ;;
+ v)
+ DEBUG=1;
+ ;;
+ p)
+ PID=$OPTARG;
+ ;;
+ x)
+ VAR=$OPTARG;
+ ;;
+ l)
+ LIMIT=$OPTARG;
+ ;;
+ w)
+ WAIT=$OPTARG;
+ ;;
+ n)
+ N=$OPTARG;
+ ;;
+ \?)
+ usage;
+ ;;
+ esac
+ done
+
+oscheck;
+set_defaults_if_noopt_given;
+validate_options;
+
+if [ $Dump_Config -eq 1 ]; then
+ dump_config;
+ exit;
+fi
+
+Done=0
+
+verbose "Trying ${N} times, Waiting ${WAIT} seconds each iteration";
+
+while [ $Done -eq 0 ]; do
+ VAL=`/bin/ps h -p $PID -o ${VAR} | perl -pe 'chomp; s/(.*)m/$1 * 1024/e; s/(.*)g/$1 * 1024 * 1024/e;'`
+ if [ ${VAL:=0} -eq 0 ]; then
+ warn "Process $PID ended without incident."
+ Done=1;
+ break;
+ fi
+
+ if [ $VAL -ge $LIMIT ]; then
+ Done=1;
+ else
+ echo "Value of '${VAR}' (${VAL}) is less than ${LIMIT} for PID ${PID}"
+ sleep $WAIT;
+ fi
+ if [ $Done -eq 1 ]; then
+
+ if [ "$ACTION" = "kill" ]; then
+ kill ${PID} || kill -3 ${PID}
+ exit;
+
+ elif [ "$ACTION" = "warn" ]; then
+
+ # go back to monitoring.
+
+ warn "`date` WARNING: ${VAR} breached threshold ${LIMIT}, actual is ${VAL}"
+ Done=0 #go back to monitoring
+
+ elif [ "$ACTION" = "die" ]; then
+ warn "WARNING: dying without killing process ${PID} on ${SERVER}"
+ warn "The process details are below: "
+ warn "`ps -p ${PID} -o pid,ppid,bsdtime,rss,vsz,cmd,args`"
+ warn ""
+
+ #should we send email/notify someone? TODO... for now, bail.
+
+ exit -1;
+
+ fi
+ else
+ :
+ #warn "INFO: PID $PID, $VAR = $VAL, limit ($LIMIT) not exceeded";
+ fi
+done
+
diff --git a/src/rocksdb/tools/reduce_levels_test.cc b/src/rocksdb/tools/reduce_levels_test.cc
new file mode 100644
index 000000000..c8604bf43
--- /dev/null
+++ b/src/rocksdb/tools/reduce_levels_test.cc
@@ -0,0 +1,222 @@
+// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+// This source code is licensed under both the GPLv2 (found in the
+// COPYING file in the root directory) and Apache 2.0 License
+// (found in the LICENSE.Apache file in the root directory).
+//
+
+#ifndef ROCKSDB_LITE
+
+#include "db/db_impl/db_impl.h"
+#include "db/version_set.h"
+#include "rocksdb/db.h"
+#include "rocksdb/utilities/ldb_cmd.h"
+#include "test_util/testharness.h"
+#include "test_util/testutil.h"
+#include "tools/ldb_cmd_impl.h"
+#include "util/cast_util.h"
+#include "util/string_util.h"
+
+namespace ROCKSDB_NAMESPACE {
+
+class ReduceLevelTest : public testing::Test {
+ public:
+ ReduceLevelTest() {
+ dbname_ = test::PerThreadDBPath("db_reduce_levels_test");
+ EXPECT_OK(DestroyDB(dbname_, Options()));
+ db_ = nullptr;
+ }
+
+ Status OpenDB(bool create_if_missing, int levels);
+
+ Status Put(const std::string& k, const std::string& v) {
+ return db_->Put(WriteOptions(), k, v);
+ }
+
+ std::string Get(const std::string& k) {
+ ReadOptions options;
+ std::string result;
+ Status s = db_->Get(options, k, &result);
+ if (s.IsNotFound()) {
+ result = "NOT_FOUND";
+ } else if (!s.ok()) {
+ result = s.ToString();
+ }
+ return result;
+ }
+
+ Status Flush() {
+ if (db_ == nullptr) {
+ return Status::InvalidArgument("DB not opened.");
+ }
+ DBImpl* db_impl = static_cast_with_check<DBImpl>(db_);
+ return db_impl->TEST_FlushMemTable();
+ }
+
+ void MoveL0FileToLevel(int level) {
+ DBImpl* db_impl = static_cast_with_check<DBImpl>(db_);
+ for (int i = 0; i < level; ++i) {
+ ASSERT_OK(db_impl->TEST_CompactRange(i, nullptr, nullptr));
+ }
+ }
+
+ void CloseDB() {
+ if (db_ != nullptr) {
+ delete db_;
+ db_ = nullptr;
+ }
+ }
+
+ bool ReduceLevels(int target_level);
+
+ int FilesOnLevel(int level) {
+ std::string property;
+ EXPECT_TRUE(db_->GetProperty(
+ "rocksdb.num-files-at-level" + std::to_string(level), &property));
+ return atoi(property.c_str());
+ }
+
+ private:
+ std::string dbname_;
+ DB* db_;
+};
+
+Status ReduceLevelTest::OpenDB(bool create_if_missing, int num_levels) {
+ ROCKSDB_NAMESPACE::Options opt;
+ opt.num_levels = num_levels;
+ opt.create_if_missing = create_if_missing;
+ ROCKSDB_NAMESPACE::Status st =
+ ROCKSDB_NAMESPACE::DB::Open(opt, dbname_, &db_);
+ if (!st.ok()) {
+ fprintf(stderr, "Can't open the db:%s\n", st.ToString().c_str());
+ }
+ return st;
+}
+
+bool ReduceLevelTest::ReduceLevels(int target_level) {
+ std::vector<std::string> args =
+ ROCKSDB_NAMESPACE::ReduceDBLevelsCommand::PrepareArgs(
+ dbname_, target_level, false);
+ LDBCommand* level_reducer = LDBCommand::InitFromCmdLineArgs(
+ args, Options(), LDBOptions(), nullptr, LDBCommand::SelectCommand);
+ level_reducer->Run();
+ bool is_succeed = level_reducer->GetExecuteState().IsSucceed();
+ delete level_reducer;
+ return is_succeed;
+}
+
+TEST_F(ReduceLevelTest, Last_Level) {
+ ASSERT_OK(OpenDB(true, 4));
+ ASSERT_OK(Put("aaaa", "11111"));
+ ASSERT_OK(Flush());
+ MoveL0FileToLevel(3);
+ ASSERT_EQ(FilesOnLevel(3), 1);
+ CloseDB();
+
+ ASSERT_TRUE(ReduceLevels(3));
+ ASSERT_OK(OpenDB(true, 3));
+ ASSERT_EQ(FilesOnLevel(2), 1);
+ CloseDB();
+
+ ASSERT_TRUE(ReduceLevels(2));
+ ASSERT_OK(OpenDB(true, 2));
+ ASSERT_EQ(FilesOnLevel(1), 1);
+ CloseDB();
+}
+
+TEST_F(ReduceLevelTest, Top_Level) {
+ ASSERT_OK(OpenDB(true, 5));
+ ASSERT_OK(Put("aaaa", "11111"));
+ ASSERT_OK(Flush());
+ ASSERT_EQ(FilesOnLevel(0), 1);
+ CloseDB();
+
+ ASSERT_TRUE(ReduceLevels(4));
+ ASSERT_OK(OpenDB(true, 4));
+ CloseDB();
+
+ ASSERT_TRUE(ReduceLevels(3));
+ ASSERT_OK(OpenDB(true, 3));
+ CloseDB();
+
+ ASSERT_TRUE(ReduceLevels(2));
+ ASSERT_OK(OpenDB(true, 2));
+ CloseDB();
+}
+
+TEST_F(ReduceLevelTest, All_Levels) {
+ ASSERT_OK(OpenDB(true, 5));
+ ASSERT_OK(Put("a", "a11111"));
+ ASSERT_OK(Flush());
+ MoveL0FileToLevel(4);
+ ASSERT_EQ(FilesOnLevel(4), 1);
+ CloseDB();
+
+ ASSERT_OK(OpenDB(true, 5));
+ ASSERT_OK(Put("b", "b11111"));
+ ASSERT_OK(Flush());
+ MoveL0FileToLevel(3);
+ ASSERT_EQ(FilesOnLevel(3), 1);
+ ASSERT_EQ(FilesOnLevel(4), 1);
+ CloseDB();
+
+ ASSERT_OK(OpenDB(true, 5));
+ ASSERT_OK(Put("c", "c11111"));
+ ASSERT_OK(Flush());
+ MoveL0FileToLevel(2);
+ ASSERT_EQ(FilesOnLevel(2), 1);
+ ASSERT_EQ(FilesOnLevel(3), 1);
+ ASSERT_EQ(FilesOnLevel(4), 1);
+ CloseDB();
+
+ ASSERT_OK(OpenDB(true, 5));
+ ASSERT_OK(Put("d", "d11111"));
+ ASSERT_OK(Flush());
+ MoveL0FileToLevel(1);
+ ASSERT_EQ(FilesOnLevel(1), 1);
+ ASSERT_EQ(FilesOnLevel(2), 1);
+ ASSERT_EQ(FilesOnLevel(3), 1);
+ ASSERT_EQ(FilesOnLevel(4), 1);
+ CloseDB();
+
+ ASSERT_TRUE(ReduceLevels(4));
+ ASSERT_OK(OpenDB(true, 4));
+ ASSERT_EQ("a11111", Get("a"));
+ ASSERT_EQ("b11111", Get("b"));
+ ASSERT_EQ("c11111", Get("c"));
+ ASSERT_EQ("d11111", Get("d"));
+ CloseDB();
+
+ ASSERT_TRUE(ReduceLevels(3));
+ ASSERT_OK(OpenDB(true, 3));
+ ASSERT_EQ("a11111", Get("a"));
+ ASSERT_EQ("b11111", Get("b"));
+ ASSERT_EQ("c11111", Get("c"));
+ ASSERT_EQ("d11111", Get("d"));
+ CloseDB();
+
+ ASSERT_TRUE(ReduceLevels(2));
+ ASSERT_OK(OpenDB(true, 2));
+ ASSERT_EQ("a11111", Get("a"));
+ ASSERT_EQ("b11111", Get("b"));
+ ASSERT_EQ("c11111", Get("c"));
+ ASSERT_EQ("d11111", Get("d"));
+ CloseDB();
+}
+
+} // namespace ROCKSDB_NAMESPACE
+
+int main(int argc, char** argv) {
+ ROCKSDB_NAMESPACE::port::InstallStackTraceHandler();
+ ::testing::InitGoogleTest(&argc, argv);
+ return RUN_ALL_TESTS();
+}
+
+#else
+#include <stdio.h>
+
+int main(int /*argc*/, char** /*argv*/) {
+ fprintf(stderr, "SKIPPED as LDBCommand is not supported in ROCKSDB_LITE\n");
+ return 0;
+}
+
+#endif // !ROCKSDB_LITE
diff --git a/src/rocksdb/tools/regression_test.sh b/src/rocksdb/tools/regression_test.sh
new file mode 100755
index 000000000..2743c5aee
--- /dev/null
+++ b/src/rocksdb/tools/regression_test.sh
@@ -0,0 +1,477 @@
+#!/usr/bin/env bash
+# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
+# The RocksDB regression test script.
+# REQUIREMENT: must be able to run make db_bench in the current directory
+#
+# This script will do the following things in order:
+#
+# 1. check out the specified rocksdb commit.
+# 2. build db_bench using the specified commit
+# 3. setup test directory $TEST_PATH. If not specified, then the test directory
+# will be "/tmp/rocksdb/regression_test"
+# 4. run set of benchmarks on the specified host
+# (can be either locally or remotely)
+# 5. generate report in the $RESULT_PATH. If RESULT_PATH is not specified,
+# RESULT_PATH will be set to $TEST_PATH/current_time
+#
+# = Examples =
+# * Run the regression test using rocksdb commit abcdef that outputs results
+# and temp files in "/my/output/dir"
+#r
+# TEST_PATH=/my/output/dir COMMIT_ID=abcdef ./tools/regression_test.sh
+#
+# * Run the regression test on a remost host under "/my/output/dir" directory
+# and stores the result locally in "/my/benchmark/results" using commit
+# abcdef and with the rocksdb options specified in /my/path/to/OPTIONS-012345
+# with 1000000000 keys in each benchmark in the regression test where each
+# key and value are 100 and 900 bytes respectively:
+#
+# REMOTE_USER_AT_HOST=yhchiang@my.remote.host \
+# TEST_PATH=/my/output/dir \
+# RESULT_PATH=/my/benchmark/results \
+# COMMIT_ID=abcdef \
+# OPTIONS_FILE=/my/path/to/OPTIONS-012345 \
+# NUM_KEYS=1000000000 \
+# KEY_SIZE=100 \
+# VALUE_SIZE=900 \
+# ./tools/regression_test.sh
+#
+# = Regression test environmental parameters =
+# DEBUG: If true, then the script will not build db_bench if db_bench already
+# exists
+# Default: 0
+# TEST_MODE: If 1, run fillseqdeterminstic and benchmarks both
+# if 0, only run fillseqdeterministc
+# if 2, only run benchmarks
+# Default: 1
+# TEST_PATH: the root directory of the regression test.
+# Default: "/tmp/rocksdb/regression_test"
+# !!! NOTE !!! - a DB will also be saved in $TEST_PATH/../db
+# RESULT_PATH: the directory where the regression results will be generated.
+# Default: "$TEST_PATH/current_time"
+# REMOTE_USER_AT_HOST: If set, then test will run on the specified host under
+# TEST_PATH directory and outputs test results locally in RESULT_PATH
+# The REMOTE_USER_AT_HOST should follow the format user-id@host.name
+# DB_PATH: the path where the rocksdb database will be created during the
+# regression test. Default: $TEST_PATH/db
+# WAL_PATH: the path where the rocksdb WAL will be outputed.
+# Default: $TEST_PATH/wal
+# OPTIONS_FILE: If specified, then the regression test will use the specified
+# file to initialize the RocksDB options in its benchmarks. Note that
+# this feature only work for commits after 88acd93 or rocksdb version
+# later than 4.9.
+# DELETE_TEST_PATH: If true, then the test directory will be deleted
+# after the script ends.
+# Default: 0
+#
+# = db_bench parameters =
+# NUM_THREADS: The number of concurrent foreground threads that will issue
+# database operations in the benchmark. Default: 16.
+# NUM_KEYS: The key range that will be used in the entire regression test.
+# Default: 1G.
+# NUM_OPS: The number of operations (reads, writes, or deletes) that will
+# be issued in EACH thread.
+# Default: $NUM_KEYS / $NUM_THREADS
+# KEY_SIZE: The size of each key in bytes in db_bench. Default: 100.
+# VALUE_SIZE: The size of each value in bytes in db_bench. Default: 900.
+# CACHE_SIZE: The size of RocksDB block cache used in db_bench. Default: 1G
+# STATISTICS: If 1, then statistics is on in db_bench. Default: 0.
+# COMPRESSION_RATIO: The compression ratio of the key generated in db_bench.
+# Default: 0.5.
+# HISTOGRAM: If 1, then the histogram feature on performance feature is on.
+# STATS_PER_INTERVAL: If 1, then the statistics will be reported for every
+# STATS_INTERVAL_SECONDS seconds. Default 1.
+# STATS_INTERVAL_SECONDS: If STATS_PER_INTERVAL is set to 1, then statistics
+# will be reported for every STATS_INTERVAL_SECONDS. Default 60.
+# MAX_BACKGROUND_FLUSHES: The maxinum number of concurrent flushes in
+# db_bench. Default: 4.
+# MAX_BACKGROUND_COMPACTIONS: The maximum number of concurrent compactions
+# in db_bench. Default: 16.
+# NUM_HIGH_PRI_THREADS: The number of high-pri threads available for
+# concurrent flushes in db_bench. Default: 4.
+# NUM_LOW_PRI_THREADS: The number of low-pri threads available for
+# concurrent compactions in db_bench. Default: 16.
+# SEEK_NEXTS: Controls how many Next() will be called after seek.
+# Default: 10.
+# SEED: random seed that controls the randomness of the benchmark.
+# Default: $( date +%s )
+
+#==============================================================================
+# CONSTANT
+#==============================================================================
+TITLE_FORMAT="%40s,%25s,%30s,%7s,%9s,%8s,"
+TITLE_FORMAT+="%10s,%13s,%14s,%11s,%12s,"
+TITLE_FORMAT+="%7s,%11s,"
+TITLE_FORMAT+="%9s,%10s,%10s,%10s,%10s,%10s,%5s,"
+TITLE_FORMAT+="%5s,%5s,%5s" # time
+TITLE_FORMAT+="\n"
+
+DATA_FORMAT="%40s,%25s,%30s,%7s,%9s,%8s,"
+DATA_FORMAT+="%10s,%13.0f,%14s,%11s,%12s,"
+DATA_FORMAT+="%7s,%11s,"
+DATA_FORMAT+="%9.0f,%10.0f,%10.0f,%10.0f,%10.0f,%10.0f,%5.0f,"
+DATA_FORMAT+="%5.0f,%5.0f,%5.0f" # time
+DATA_FORMAT+="\n"
+
+MAIN_PATTERN="$1""[[:blank:]]+:.*[[:blank:]]+([0-9\.]+)[[:blank:]]+ops/sec"
+PERC_PATTERN="Percentiles: P50: ([0-9\.]+) P75: ([0-9\.]+) "
+PERC_PATTERN+="P99: ([0-9\.]+) P99.9: ([0-9\.]+) P99.99: ([0-9\.]+)"
+#==============================================================================
+
+function main {
+ TEST_ROOT_DIR=${TEST_PATH:-"/tmp/rocksdb/regression_test"}
+ init_arguments $TEST_ROOT_DIR
+
+ build_db_bench_and_ldb
+
+ setup_test_directory
+ if [ $TEST_MODE -le 1 ]; then
+ test_remote "test -d $ORIGIN_PATH"
+ if [[ $? -ne 0 ]]; then
+ echo "Building DB..."
+ # compactall alone will not print ops or threads, which will fail update_report
+ run_db_bench "fillseq,compactall" $NUM_KEYS 1 0 0
+ # only save for future use on success
+ test_remote "mv $DB_PATH $ORIGIN_PATH"
+ fi
+ fi
+ if [ $TEST_MODE -ge 1 ]; then
+ build_checkpoint
+ run_db_bench "readrandom"
+ run_db_bench "readwhilewriting"
+ run_db_bench "deleterandom"
+ run_db_bench "seekrandom"
+ run_db_bench "seekrandomwhilewriting"
+ run_db_bench "multireadrandom"
+ fi
+
+ cleanup_test_directory $TEST_ROOT_DIR
+ echo ""
+ echo "Benchmark completed! Results are available in $RESULT_PATH"
+}
+
+############################################################################
+function init_arguments {
+ K=1024
+ M=$((1024 * K))
+ G=$((1024 * M))
+
+ current_time=$(date +"%F-%H:%M:%S")
+ RESULT_PATH=${RESULT_PATH:-"$1/results/$current_time"}
+ COMMIT_ID=`hg id -i 2>/dev/null || git rev-parse HEAD 2>/dev/null || echo 'unknown'`
+ SUMMARY_FILE="$RESULT_PATH/SUMMARY.csv"
+
+ DB_PATH=${3:-"$1/db"}
+ ORIGIN_PATH=${ORIGIN_PATH:-"$(dirname $(dirname $DB_PATH))/db"}
+ WAL_PATH=${4:-""}
+ if [ -z "$REMOTE_USER_AT_HOST" ]; then
+ DB_BENCH_DIR=${5:-"."}
+ else
+ DB_BENCH_DIR=${5:-"$1/db_bench"}
+ fi
+
+ DEBUG=${DEBUG:-0}
+ TEST_MODE=${TEST_MODE:-1}
+ SCP=${SCP:-"scp"}
+ SSH=${SSH:-"ssh"}
+ NUM_THREADS=${NUM_THREADS:-16}
+ NUM_KEYS=${NUM_KEYS:-$((1 * G))} # key range
+ NUM_OPS=${NUM_OPS:-$(($NUM_KEYS / $NUM_THREADS))}
+ KEY_SIZE=${KEY_SIZE:-100}
+ VALUE_SIZE=${VALUE_SIZE:-900}
+ CACHE_SIZE=${CACHE_SIZE:-$((1 * G))}
+ STATISTICS=${STATISTICS:-0}
+ COMPRESSION_RATIO=${COMPRESSION_RATIO:-0.5}
+ HISTOGRAM=${HISTOGRAM:-1}
+ NUM_MULTI_DB=${NUM_MULTI_DB:-1}
+ STATS_PER_INTERVAL=${STATS_PER_INTERVAL:-1}
+ STATS_INTERVAL_SECONDS=${STATS_INTERVAL_SECONDS:-600}
+ MAX_BACKGROUND_FLUSHES=${MAX_BACKGROUND_FLUSHES:-4}
+ MAX_BACKGROUND_COMPACTIONS=${MAX_BACKGROUND_COMPACTIONS:-16}
+ NUM_HIGH_PRI_THREADS=${NUM_HIGH_PRI_THREADS:-4}
+ NUM_LOW_PRI_THREADS=${NUM_LOW_PRI_THREADS:-16}
+ DELETE_TEST_PATH=${DELETE_TEST_PATH:-0}
+ SEEK_NEXTS=${SEEK_NEXTS:-10}
+ SEED=${SEED:-$( date +%s )}
+ MULTIREAD_BATCH_SIZE=${MULTIREAD_BATCH_SIZE:-128}
+ MULTIREAD_STRIDE=${MULTIREAD_STRIDE:-12}
+ PERF_LEVEL=${PERF_LEVEL:-1}
+}
+
+# $1 --- benchmark name
+# $2 --- number of operations. Default: $NUM_KEYS
+# $3 --- number of threads. Default $NUM_THREADS
+# $4 --- use_existing_db. Default: 1
+# $5 --- update_report. Default: 1
+function run_db_bench {
+ # Make sure no other db_bench is running. (Make sure command succeeds if pidof
+ # command exists but finds nothing.)
+ pids_cmd='pidof db_bench || pidof --version > /dev/null'
+ # But first, make best effort to kill any db_bench that have run for more
+ # than 12 hours, as that indicates a hung or runaway process.
+ kill_old_cmd='for PID in $(pidof db_bench); do [ "$(($(stat -c %Y /proc/$PID) + 43200))" -lt "$(date +%s)" ] && echo "Killing old db_bench $PID" && kill $PID && sleep 5 && kill -9 $PID && sleep 5; done; pidof --version > /dev/null'
+ if ! [ -z "$REMOTE_USER_AT_HOST" ]; then
+ pids_cmd="$SSH $REMOTE_USER_AT_HOST '$pids_cmd'"
+ kill_old_cmd="$SSH $REMOTE_USER_AT_HOST '$kill_old_cmd'"
+ fi
+
+ eval $kill_old_cmd
+ exit_on_error $? "$kill_old_cmd"
+
+ pids_output="$(eval $pids_cmd)"
+ exit_on_error $? "$pids_cmd"
+
+ if [ "$pids_output" != "" ]; then
+ echo "Stopped regression_test.sh as there're still recent db_bench "
+ echo "processes running: $pids_output"
+ echo "Clean up test directory"
+ cleanup_test_directory $TEST_ROOT_DIR
+ exit 2
+ fi
+
+ # Build db_bench command
+ ops=${2:-$NUM_OPS}
+ threads=${3:-$NUM_THREADS}
+ USE_EXISTING_DB=${4:-1}
+ UPDATE_REPORT=${5:-1}
+ echo ""
+ echo "======================================================================="
+ echo "Benchmark $1"
+ echo "======================================================================="
+ echo ""
+ db_bench_error=0
+ options_file_arg=$(setup_options_file)
+ echo "$options_file_arg"
+ # use `which time` to avoid using bash's internal time command
+ db_bench_cmd="\$(which time) -p $DB_BENCH_DIR/db_bench \
+ --benchmarks=$1 --db=$DB_PATH --wal_dir=$WAL_PATH \
+ --use_existing_db=$USE_EXISTING_DB \
+ --perf_level=$PERF_LEVEL \
+ --disable_auto_compactions \
+ --threads=$threads \
+ --num=$NUM_KEYS \
+ --reads=$ops \
+ --writes=$ops \
+ --deletes=$ops \
+ --key_size=$KEY_SIZE \
+ --value_size=$VALUE_SIZE \
+ --cache_size=$CACHE_SIZE \
+ --statistics=$STATISTICS \
+ $options_file_arg \
+ --compression_ratio=$COMPRESSION_RATIO \
+ --histogram=$HISTOGRAM \
+ --seek_nexts=$SEEK_NEXTS \
+ --stats_per_interval=$STATS_PER_INTERVAL \
+ --stats_interval_seconds=$STATS_INTERVAL_SECONDS \
+ --max_background_flushes=$MAX_BACKGROUND_FLUSHES \
+ --num_multi_db=$NUM_MULTI_DB \
+ --max_background_compactions=$MAX_BACKGROUND_COMPACTIONS \
+ --num_high_pri_threads=$NUM_HIGH_PRI_THREADS \
+ --num_low_pri_threads=$NUM_LOW_PRI_THREADS \
+ --seed=$SEED \
+ --multiread_batched=true \
+ --batch_size=$MULTIREAD_BATCH_SIZE \
+ --multiread_stride=$MULTIREAD_STRIDE 2>&1"
+ if ! [ -z "$REMOTE_USER_AT_HOST" ]; then
+ echo "Running benchmark remotely on $REMOTE_USER_AT_HOST"
+ db_bench_cmd="$SSH $REMOTE_USER_AT_HOST '$db_bench_cmd'"
+ fi
+ echo db_bench_cmd="$db_bench_cmd"
+
+ # Run the db_bench command
+ eval $db_bench_cmd | tee -a "$RESULT_PATH/$1"
+ exit_on_error ${PIPESTATUS[0]} db_bench
+ if [ $UPDATE_REPORT -ne 0 ]; then
+ update_report "$1" "$RESULT_PATH/$1" $ops $threads
+ fi
+}
+
+function build_checkpoint {
+ cmd_prefix=""
+ if ! [ -z "$REMOTE_USER_AT_HOST" ]; then
+ cmd_prefix="$SSH $REMOTE_USER_AT_HOST "
+ fi
+ if [ $NUM_MULTI_DB -gt 1 ]; then
+ dirs=$($cmd_prefix find $ORIGIN_PATH -type d -links 2)
+ for dir in $dirs; do
+ db_index=$(basename $dir)
+ echo "Building checkpoints: $ORIGIN_PATH/$db_index -> $DB_PATH/$db_index ..."
+ $cmd_prefix $DB_BENCH_DIR/ldb checkpoint --checkpoint_dir=$DB_PATH/$db_index \
+ --db=$ORIGIN_PATH/$db_index --try_load_options 2>&1
+ exit_on_error $?
+ done
+ else
+ # checkpoint cannot build in directory already exists
+ $cmd_prefix rm -rf $DB_PATH
+ echo "Building checkpoint: $ORIGIN_PATH -> $DB_PATH ..."
+ $cmd_prefix $DB_BENCH_DIR/ldb checkpoint --checkpoint_dir=$DB_PATH \
+ --db=$ORIGIN_PATH --try_load_options 2>&1
+ exit_on_error $?
+ fi
+}
+
+function multiply {
+ echo "$1 * $2" | bc
+}
+
+# $1 --- name of the benchmark
+# $2 --- the filename of the output log of db_bench
+function update_report {
+ main_result=`cat $2 | grep $1`
+ exit_on_error $?
+ perc_statement=`cat $2 | grep Percentile`
+ exit_on_error $?
+
+ # Obtain micros / op
+
+ [[ $main_result =~ $MAIN_PATTERN ]]
+ ops_per_s=${BASH_REMATCH[1]}
+
+ # Obtain percentile information
+ [[ $perc_statement =~ $PERC_PATTERN ]]
+ perc[0]=${BASH_REMATCH[1]} # p50
+ perc[1]=${BASH_REMATCH[2]} # p75
+ perc[2]=${BASH_REMATCH[3]} # p99
+ perc[3]=${BASH_REMATCH[4]} # p99.9
+ perc[4]=${BASH_REMATCH[5]} # p99.99
+
+ # Parse the output of the time command
+ real_sec=`tail -3 $2 | grep real | awk '{print $2}'`
+ user_sec=`tail -3 $2 | grep user | awk '{print $2}'`
+ sys_sec=`tail -3 $2 | grep sys | awk '{print $2}'`
+
+ (printf "$DATA_FORMAT" \
+ $COMMIT_ID $1 $REMOTE_USER_AT_HOST $NUM_MULTI_DB $NUM_KEYS $KEY_SIZE $VALUE_SIZE \
+ $(multiply $COMPRESSION_RATIO 100) \
+ $3 $4 $CACHE_SIZE \
+ $MAX_BACKGROUND_FLUSHES $MAX_BACKGROUND_COMPACTIONS \
+ $ops_per_s \
+ $(multiply ${perc[0]} 1000) \
+ $(multiply ${perc[1]} 1000) \
+ $(multiply ${perc[2]} 1000) \
+ $(multiply ${perc[3]} 1000) \
+ $(multiply ${perc[4]} 1000) \
+ $DEBUG \
+ $real_sec \
+ $user_sec \
+ $sys_sec \
+ >> $SUMMARY_FILE)
+ exit_on_error $?
+}
+
+function exit_on_error {
+ if [ $1 -ne 0 ]; then
+ echo ""
+ echo "ERROR: Benchmark did not complete successfully."
+ if ! [ -z "$2" ]; then
+ echo "Failure command: $2"
+ fi
+ echo "Partial results are output to $RESULT_PATH"
+ echo "ERROR" >> $SUMMARY_FILE
+ exit $1
+ fi
+}
+
+function build_db_bench_and_ldb {
+ echo "Building db_bench & ldb ..."
+
+ make clean
+ exit_on_error $?
+
+ DEBUG_LEVEL=0 make db_bench ldb -j32
+ exit_on_error $?
+}
+
+function run_remote {
+ test_remote "$1"
+ exit_on_error $? "$1"
+}
+
+function test_remote {
+ if ! [ -z "$REMOTE_USER_AT_HOST" ]; then
+ cmd="$SSH $REMOTE_USER_AT_HOST '$1'"
+ else
+ cmd="$1"
+ fi
+ eval "$cmd"
+}
+
+function run_local {
+ eval "$1"
+ exit_on_error $? "$1"
+}
+
+function setup_options_file {
+ if ! [ -z "$OPTIONS_FILE" ]; then
+ if ! [ -z "$REMOTE_USER_AT_HOST" ]; then
+ options_file="$DB_BENCH_DIR/OPTIONS_FILE"
+ run_local "$SCP $OPTIONS_FILE $REMOTE_USER_AT_HOST:$options_file"
+ else
+ options_file="$OPTIONS_FILE"
+ fi
+ echo "--options_file=$options_file"
+ fi
+ echo ""
+}
+
+function setup_test_directory {
+ echo "Deleting old regression test directories and creating new ones"
+
+ run_local 'test "$DB_PATH" != "."'
+ run_remote "rm -rf $DB_PATH"
+
+ if [ "$DB_BENCH_DIR" != "." ]; then
+ run_remote "rm -rf $DB_BENCH_DIR"
+ fi
+
+ run_local 'test "$RESULT_PATH" != "."'
+ run_local "rm -rf $RESULT_PATH"
+
+ if ! [ -z "$WAL_PATH" ]; then
+ run_remote "rm -rf $WAL_PATH"
+ run_remote "mkdir -p $WAL_PATH"
+ fi
+
+ run_remote "mkdir -p $DB_PATH"
+
+ run_remote "mkdir -p $DB_BENCH_DIR"
+ run_remote "ls -l $DB_BENCH_DIR"
+
+ if ! [ -z "$REMOTE_USER_AT_HOST" ]; then
+ run_local "$SCP ./db_bench $REMOTE_USER_AT_HOST:$DB_BENCH_DIR/db_bench"
+ run_local "$SCP ./ldb $REMOTE_USER_AT_HOST:$DB_BENCH_DIR/ldb"
+ fi
+
+ run_local "mkdir -p $RESULT_PATH"
+
+ (printf $TITLE_FORMAT \
+ "commit id" "benchmark" "user@host" "num-dbs" "key-range" "key-size" \
+ "value-size" "compress-rate" "ops-per-thread" "num-threads" "cache-size" \
+ "flushes" "compactions" \
+ "ops-per-s" "p50" "p75" "p99" "p99.9" "p99.99" "debug" \
+ "real-sec" "user-sec" "sys-sec" \
+ >> $SUMMARY_FILE)
+ exit_on_error $?
+}
+
+function cleanup_test_directory {
+
+ if [ $DELETE_TEST_PATH -ne 0 ]; then
+ echo "Clear old regression test directories and creating new ones"
+ run_remote "rm -rf $DB_PATH"
+ run_remote "rm -rf $WAL_PATH"
+ if ! [ -z "$REMOTE_USER_AT_HOST" ]; then
+ run_remote "rm -rf $DB_BENCH_DIR"
+ fi
+ run_remote "rm -rf $1"
+ else
+ echo "------------ DEBUG MODE ------------"
+ echo "DB PATH: $DB_PATH"
+ echo "WAL PATH: $WAL_PATH"
+ fi
+}
+
+############################################################################
+
+# shellcheck disable=SC2068
+main $@
diff --git a/src/rocksdb/tools/restore_db.sh b/src/rocksdb/tools/restore_db.sh
new file mode 100755
index 000000000..ed89794b2
--- /dev/null
+++ b/src/rocksdb/tools/restore_db.sh
@@ -0,0 +1,15 @@
+#!/usr/bin/env bash
+# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
+#
+#
+
+if [ "$#" -lt 2 ]; then
+ echo "usage: ${BASH_SOURCE[0]} <Backup Dir> <DB Path>"
+ exit 1
+fi
+
+backup_dir="$1"
+db_dir="$2"
+
+echo "== Restoring latest from $backup_dir to $db_dir"
+./ldb restore --db="$db_dir" --backup_dir="$backup_dir"
diff --git a/src/rocksdb/tools/rocksdb_dump_test.sh b/src/rocksdb/tools/rocksdb_dump_test.sh
new file mode 100755
index 000000000..532c53267
--- /dev/null
+++ b/src/rocksdb/tools/rocksdb_dump_test.sh
@@ -0,0 +1,9 @@
+# shellcheck disable=SC2148
+# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
+TESTDIR=`mktemp -d ${TMPDIR:-/tmp}/rocksdb-dump-test.XXXXX`
+DUMPFILE="tools/sample-dump.dmp"
+
+# Verify that the sample dump file is undumpable and then redumpable.
+./rocksdb_undump --dump_location=$DUMPFILE --db_path=$TESTDIR/db
+./rocksdb_dump --anonymous --db_path=$TESTDIR/db --dump_location=$TESTDIR/dump
+cmp $DUMPFILE $TESTDIR/dump
diff --git a/src/rocksdb/tools/run_blob_bench.sh b/src/rocksdb/tools/run_blob_bench.sh
new file mode 100755
index 000000000..3755a9e56
--- /dev/null
+++ b/src/rocksdb/tools/run_blob_bench.sh
@@ -0,0 +1,223 @@
+#!/usr/bin/env bash
+# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
+#
+# BlobDB benchmark script
+#
+# REQUIRES: benchmark.sh is in the tools subdirectory
+#
+# After the execution of this script, log files are available in $output_dir.
+# report.tsv provides high level statistics.
+#
+# Should be run from the parent of the tools directory. The command line is:
+# [$env_vars] tools/run_blob_bench.sh
+#
+# This runs the following sequence of BlobDB performance tests:
+# phase 1) write-only - bulkload+compact, overwrite+waitforcompaction
+# phase 2) read-write - readwhilewriting, fwdrangewhilewriting
+# phase 3) read-only - readrandom, fwdrange
+#
+
+# Exit Codes
+EXIT_INVALID_ARGS=1
+
+# Size constants
+K=1024
+M=$((1024 * K))
+G=$((1024 * M))
+T=$((1024 * G))
+
+function display_usage() {
+ echo "usage: run_blob_bench.sh [--help]"
+ echo ""
+ echo "Runs the following sequence of BlobDB benchmark tests using tools/benchmark.sh:"
+ echo -e "\tPhase 1: write-only tests: bulkload+compact, overwrite+waitforcompaction"
+ echo -e "\tPhase 2: read-write tests: readwhilewriting, fwdrangewhilewriting"
+ echo -e "\tPhase 3: read-only tests: readrandom, fwdrange"
+ echo ""
+ echo "Environment Variables:"
+ echo -e "\tJOB_ID\t\t\t\tIdentifier for the benchmark job, will appear in the results (default: empty)"
+ echo -e "\tDB_DIR\t\t\t\tPath for the RocksDB data directory (mandatory)"
+ echo -e "\tWAL_DIR\t\t\t\tPath for the RocksDB WAL directory (mandatory)"
+ echo -e "\tOUTPUT_DIR\t\t\tPath for the benchmark results (mandatory)"
+ echo -e "\tNUM_THREADS\t\t\tNumber of threads (default: 16)"
+ echo -e "\tCOMPRESSION_TYPE\t\tCompression type for the SST files (default: lz4)"
+ echo -e "\tDB_SIZE\t\t\t\tRaw (uncompressed) database size (default: 1 TB)"
+ echo -e "\tVALUE_SIZE\t\t\tValue size (default: 1 KB)"
+ echo -e "\tNUM_KEYS\t\t\tNumber of keys (default: raw database size divided by value size)"
+ echo -e "\tDURATION\t\t\tIndividual duration for read-write/read-only tests in seconds (default: 1800)"
+ echo -e "\tWRITE_BUFFER_SIZE\t\tWrite buffer (memtable) size (default: 1 GB)"
+ echo -e "\tENABLE_BLOB_FILES\t\tEnable blob files (default: 1)"
+ echo -e "\tMIN_BLOB_SIZE\t\t\tSize threshold for storing values in blob files (default: 0)"
+ echo -e "\tBLOB_FILE_SIZE\t\t\tBlob file size (default: same as write buffer size)"
+ echo -e "\tBLOB_COMPRESSION_TYPE\t\tCompression type for the blob files (default: lz4)"
+ echo -e "\tENABLE_BLOB_GC\t\t\tEnable blob garbage collection (default: 1)"
+ echo -e "\tBLOB_GC_AGE_CUTOFF\t\tBlob garbage collection age cutoff (default: 0.25)"
+ echo -e "\tBLOB_GC_FORCE_THRESHOLD\t\tThreshold for forcing garbage collection of the oldest blob files (default: 1.0)"
+ echo -e "\tBLOB_COMPACTION_READAHEAD_SIZE\tBlob compaction readahead size (default: 0)"
+ echo -e "\tBLOB_FILE_STARTING_LEVEL\t\tBlob file starting level (default: 0)"
+ echo -e "\tUSE_BLOB_CACHE\t\t\tEnable blob cache. (default: 1)"
+ echo -e "\tUSE_SHARED_BLOCK_AND_BLOB_CACHE\t\t\tUse the same backing cache for block cache and blob cache. (default: 1)"
+ echo -e "\tBLOB_CACHE_SIZE\t\t\tSize of the blob cache (default: 16GB)"
+ echo -e "\tBLOB_CACHE_NUMSHARDBITS\t\t\tNumber of shards for the blob cache is 2 ** blob_cache_numshardbits (default: 6)"
+ echo -e "\tPREPOPULATE_BLOB_CACHE\t\t\tPre-populate hot/warm blobs in blob cache (default: 0)"
+ echo -e "\tTARGET_FILE_SIZE_BASE\t\tTarget SST file size for compactions (default: write buffer size, scaled down if blob files are enabled)"
+ echo -e "\tMAX_BYTES_FOR_LEVEL_BASE\tMaximum size for the base level (default: 8 * target SST file size)"
+}
+
+if [ $# -ge 1 ]; then
+ display_usage
+
+ if [ "$1" == "--help" ]; then
+ exit
+ else
+ exit $EXIT_INVALID_ARGS
+ fi
+fi
+
+# shellcheck disable=SC2153
+if [ -z "$DB_DIR" ]; then
+ echo "DB_DIR is not defined"
+ exit $EXIT_INVALID_ARGS
+fi
+
+# shellcheck disable=SC2153
+if [ -z "$WAL_DIR" ]; then
+ echo "WAL_DIR is not defined"
+ exit $EXIT_INVALID_ARGS
+fi
+
+# shellcheck disable=SC2153
+if [ -z "$OUTPUT_DIR" ]; then
+ echo "OUTPUT_DIR is not defined"
+ exit $EXIT_INVALID_ARGS
+fi
+
+# shellcheck disable=SC2153
+job_id=$JOB_ID
+
+db_dir=$DB_DIR
+wal_dir=$WAL_DIR
+output_dir=$OUTPUT_DIR
+
+num_threads=${NUM_THREADS:-16}
+
+compression_type=${COMPRESSION_TYPE:-lz4}
+
+db_size=${DB_SIZE:-$((1 * T))}
+value_size=${VALUE_SIZE:-$((1 * K))}
+num_keys=${NUM_KEYS:-$((db_size / value_size))}
+
+duration=${DURATION:-1800}
+
+write_buffer_size=${WRITE_BUFFER_SIZE:-$((1 * G))}
+
+enable_blob_files=${ENABLE_BLOB_FILES:-1}
+min_blob_size=${MIN_BLOB_SIZE:-0}
+blob_file_size=${BLOB_FILE_SIZE:-$write_buffer_size}
+blob_compression_type=${BLOB_COMPRESSION_TYPE:-lz4}
+enable_blob_garbage_collection=${ENABLE_BLOB_GC:-1}
+blob_garbage_collection_age_cutoff=${BLOB_GC_AGE_CUTOFF:-0.25}
+blob_garbage_collection_force_threshold=${BLOB_GC_FORCE_THRESHOLD:-1.0}
+blob_compaction_readahead_size=${BLOB_COMPACTION_READAHEAD_SIZE:-0}
+blob_file_starting_level=${BLOB_FILE_STARTING_LEVEL:-0}
+use_blob_cache=${USE_BLOB_CACHE:-1}
+use_shared_block_and_blob_cache=${USE_SHARED_BLOCK_AND_BLOB_CACHE:-1}
+blob_cache_size=${BLOB_CACHE_SIZE:-$((16 * G))}
+blob_cache_numshardbits=${BLOB_CACHE_NUMSHARDBITS:-6}
+prepopulate_blob_cache=${PREPOPULATE_BLOB_CACHE:-0}
+
+if [ "$enable_blob_files" == "1" ]; then
+ target_file_size_base=${TARGET_FILE_SIZE_BASE:-$((32 * write_buffer_size / value_size))}
+else
+ target_file_size_base=${TARGET_FILE_SIZE_BASE:-$write_buffer_size}
+fi
+
+max_bytes_for_level_base=${MAX_BYTES_FOR_LEVEL_BASE:-$((8 * target_file_size_base))}
+
+echo "======================== Benchmark setup ========================"
+echo -e "Job ID:\t\t\t\t\t$job_id"
+echo -e "Data directory:\t\t\t\t$db_dir"
+echo -e "WAL directory:\t\t\t\t$wal_dir"
+echo -e "Output directory:\t\t\t$output_dir"
+echo -e "Number of threads:\t\t\t$num_threads"
+echo -e "Compression type for SST files:\t\t$compression_type"
+echo -e "Raw database size:\t\t\t$db_size"
+echo -e "Value size:\t\t\t\t$value_size"
+echo -e "Number of keys:\t\t\t\t$num_keys"
+echo -e "Duration of read-write/read-only tests:\t$duration"
+echo -e "Write buffer size:\t\t\t$write_buffer_size"
+echo -e "Blob files enabled:\t\t\t$enable_blob_files"
+echo -e "Blob size threshold:\t\t\t$min_blob_size"
+echo -e "Blob file size:\t\t\t\t$blob_file_size"
+echo -e "Compression type for blob files:\t$blob_compression_type"
+echo -e "Blob GC enabled:\t\t\t$enable_blob_garbage_collection"
+echo -e "Blob GC age cutoff:\t\t\t$blob_garbage_collection_age_cutoff"
+echo -e "Blob GC force threshold:\t\t$blob_garbage_collection_force_threshold"
+echo -e "Blob compaction readahead size:\t\t$blob_compaction_readahead_size"
+echo -e "Blob file starting level:\t\t$blob_file_starting_level"
+echo -e "Blob cache enabled:\t\t\t$use_blob_cache"
+echo -e "Blob cache and block cache shared:\t\t\t$use_shared_block_and_blob_cache"
+echo -e "Blob cache size:\t\t$blob_cache_size"
+echo -e "Blob cache number of shard bits:\t\t$blob_cache_numshardbits"
+echo -e "Blob cache prepopulated:\t\t\t$prepopulate_blob_cache"
+echo -e "Target SST file size:\t\t\t$target_file_size_base"
+echo -e "Maximum size of base level:\t\t$max_bytes_for_level_base"
+echo "================================================================="
+
+rm -rf "$db_dir"
+rm -rf "$wal_dir"
+rm -rf "$output_dir"
+
+ENV_VARS="\
+ JOB_ID=$job_id \
+ DB_DIR=$db_dir \
+ WAL_DIR=$wal_dir \
+ OUTPUT_DIR=$output_dir \
+ NUM_THREADS=$num_threads \
+ COMPRESSION_TYPE=$compression_type \
+ VALUE_SIZE=$value_size \
+ NUM_KEYS=$num_keys"
+
+ENV_VARS_D="$ENV_VARS DURATION=$duration"
+
+PARAMS="\
+ --enable_blob_files=$enable_blob_files \
+ --min_blob_size=$min_blob_size \
+ --blob_file_size=$blob_file_size \
+ --blob_compression_type=$blob_compression_type \
+ --blob_file_starting_level=$blob_file_starting_level \
+ --use_blob_cache=$use_blob_cache \
+ --use_shared_block_and_blob_cache=$use_shared_block_and_blob_cache \
+ --blob_cache_size=$blob_cache_size \
+ --blob_cache_numshardbits=$blob_cache_numshardbits \
+ --prepopulate_blob_cache=$prepopulate_blob_cache \
+ --write_buffer_size=$write_buffer_size \
+ --target_file_size_base=$target_file_size_base \
+ --max_bytes_for_level_base=$max_bytes_for_level_base"
+
+PARAMS_GC="$PARAMS \
+ --enable_blob_garbage_collection=$enable_blob_garbage_collection \
+ --blob_garbage_collection_age_cutoff=$blob_garbage_collection_age_cutoff \
+ --blob_garbage_collection_force_threshold=$blob_garbage_collection_force_threshold \
+ --blob_compaction_readahead_size=$blob_compaction_readahead_size"
+
+# bulk load (using fillrandom) + compact
+env -u DURATION -S "$ENV_VARS" ./tools/benchmark.sh bulkload "$PARAMS"
+
+# overwrite + waitforcompaction
+env -u DURATION -S "$ENV_VARS" ./tools/benchmark.sh overwrite "$PARAMS_GC"
+
+# readwhilewriting
+env -S "$ENV_VARS_D" ./tools/benchmark.sh readwhilewriting "$PARAMS_GC"
+
+# fwdrangewhilewriting
+env -S "$ENV_VARS_D" ./tools/benchmark.sh fwdrangewhilewriting "$PARAMS_GC"
+
+# readrandom
+env -S "$ENV_VARS_D" ./tools/benchmark.sh readrandom "$PARAMS_GC"
+
+# fwdrange
+env -S "$ENV_VARS_D" ./tools/benchmark.sh fwdrange "$PARAMS_GC"
+
+# save logs to output directory
+cp "$db_dir"/LOG* "$output_dir/"
diff --git a/src/rocksdb/tools/run_flash_bench.sh b/src/rocksdb/tools/run_flash_bench.sh
new file mode 100755
index 000000000..26e253843
--- /dev/null
+++ b/src/rocksdb/tools/run_flash_bench.sh
@@ -0,0 +1,359 @@
+#!/usr/bin/env bash
+# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
+# REQUIRE: benchmark.sh exists in the current directory
+# After execution of this script, log files are generated in $output_dir.
+# report.txt provides a high level statistics
+
+# This should be run from the parent of the tools directory. The command line is:
+# [$env_vars] tools/run_flash_bench.sh [list-of-threads]
+#
+# This runs a sequence of tests in the following sequence:
+# step 1) load - bulkload, compact, fillseq, overwrite
+# step 2) read-only for each number of threads
+# step 3) read-write for each number of threads
+# step 4) merge for each number of threads
+#
+# The list of threads is optional and when not set is equivalent to "24".
+# Were list-of-threads specified as "1 2 4" then the tests in steps 2, 3 and
+# 4 above would be repeated for 1, 2 and 4 threads. The tests in step 1 are
+# only run for 1 thread.
+
+# Test output is written to $OUTPUT_DIR, currently /tmp/output. The performance
+# summary is in $OUTPUT_DIR/report.txt. There is one file in $OUTPUT_DIR per
+# test and the tests are listed below.
+#
+# The environment variables are also optional. The variables are:
+#
+# NKEYS - number of key/value pairs to load
+# BG_MBWRITEPERSEC - write rate limit in MB/second for tests in which
+# there is one thread doing writes and stats are
+# reported for read threads. "BG" stands for background.
+# If this is too large then the non-writer threads can get
+# starved. This is used for the "readwhile" tests.
+# FG_MBWRITEPERSEC - write rate limit in MB/second for tests like overwrite
+# where stats are reported for the write threads.
+# NSECONDS - number of seconds for which to run each test in steps 2,
+# 3 and 4. There are currently 15 tests in those steps and
+# they are repeated for each entry in list-of-threads so
+# this variable lets you control the total duration to
+# finish the benchmark.
+# RANGE_LIMIT - the number of rows to read per range query for tests that
+# do range queries.
+# VAL_SIZE - the length of the value in the key/value pairs loaded.
+# You can estimate the size of the test database from this,
+# NKEYS and the compression rate (--compression_ratio) set
+# in tools/benchmark.sh
+# BLOCK_LENGTH - value for db_bench --block_size
+# CACHE_BYTES - the size of the RocksDB block cache in bytes
+# DATA_DIR - directory in which to create database files
+# LOG_DIR - directory in which to create WAL files, may be the same
+# as DATA_DIR
+# DO_SETUP - when set to 0 then a backup of the database is copied from
+# $DATA_DIR.bak to $DATA_DIR and the load tests from step 1
+# The WAL directory is also copied from a backup if
+# DATA_DIR != LOG_DIR. This allows tests from steps 2, 3, 4
+# to be repeated faster.
+# SAVE_SETUP - saves a copy of the database at the end of step 1 to
+# $DATA_DIR.bak. When LOG_DIR != DATA_DIR then it is copied
+# to $LOG_DIR.bak.
+# SKIP_LOW_PRI_TESTS - skip some of the tests which aren't crucial for getting
+# actionable benchmarking data (look for keywords "bulkload",
+# "sync=1", and "while merging").
+#
+
+# Size constants
+K=1024
+M=$((1024 * K))
+G=$((1024 * M))
+
+num_keys=${NKEYS:-$((1 * G))}
+# write rate for readwhile... tests
+bg_mbwps=${BG_MBWRITEPERSEC:-4}
+# write rate for tests other than readwhile, 0 means no limit
+fg_mbwps=${FG_MBWRITEPERSEC:-0}
+duration=${NSECONDS:-$((60 * 60))}
+nps=${RANGE_LIMIT:-10}
+vs=${VAL_SIZE:-400}
+cs=${CACHE_BYTES:-$(( 1 * G ))}
+bs=${BLOCK_LENGTH:-8192}
+
+# If no command line arguments then run for 24 threads.
+if [[ $# -eq 0 ]]; then
+ nthreads=( 24 )
+else
+ nthreads=( "$@" )
+fi
+
+for num_thr in "${nthreads[@]}" ; do
+ echo Will run for $num_thr threads
+done
+
+# Update these parameters before execution !!!
+db_dir=${DATA_DIR:-"/tmp/rocksdb/"}
+wal_dir=${LOG_DIR:-"/tmp/rocksdb/"}
+
+do_setup=${DO_SETUP:-1}
+save_setup=${SAVE_SETUP:-0}
+
+# By default we'll run all the tests. Set this to skip a set of tests which
+# aren't critical for getting key metrics.
+skip_low_pri_tests=${SKIP_LOW_PRI_TESTS:-0}
+
+if [[ $skip_low_pri_tests == 1 ]]; then
+ echo "Skipping some non-critical tests because SKIP_LOW_PRI_TESTS is set."
+fi
+
+output_dir="${TMPDIR:-/tmp}/output"
+
+ARGS="\
+OUTPUT_DIR=$output_dir \
+NUM_KEYS=$num_keys \
+DB_DIR=$db_dir \
+WAL_DIR=$wal_dir \
+VALUE_SIZE=$vs \
+BLOCK_SIZE=$bs \
+CACHE_SIZE=$cs"
+
+mkdir -p $output_dir
+echo -e "ops/sec\tmb/sec\tSize-GB\tL0_GB\tSum_GB\tW-Amp\tW-MB/s\tusec/op\tp50\tp75\tp99\tp99.9\tp99.99\tUptime\tStall-time\tStall%\tTest" \
+ > $output_dir/report.txt
+
+# Notes on test sequence:
+# step 1) Setup database via sequential fill followed by overwrite to fragment it.
+# Done without setting DURATION to make sure that overwrite does $num_keys writes
+# step 2) read-only tests for all levels of concurrency requested
+# step 3) non read-only tests for all levels of concurrency requested
+# step 4) merge tests for all levels of concurrency requested. These must come last.
+
+###### Setup the database
+
+if [[ $do_setup != 0 ]]; then
+ echo Doing setup
+
+ if [[ $skip_low_pri_tests != 1 ]]; then
+ # Test 1: bulk load
+ env $ARGS ./tools/benchmark.sh bulkload
+ fi
+
+ # Test 2a: sequential fill with large values to get peak ingest
+ # adjust NUM_KEYS given the use of larger values
+ env $ARGS BLOCK_SIZE=$((1 * M)) VALUE_SIZE=$((32 * K)) NUM_KEYS=$(( num_keys / 64 )) \
+ ./tools/benchmark.sh fillseq_disable_wal
+
+ # Test 2b: sequential fill with the configured value size
+ env $ARGS ./tools/benchmark.sh fillseq_disable_wal
+
+ # Test 2c: same as 2a, but with WAL being enabled.
+ env $ARGS BLOCK_SIZE=$((1 * M)) VALUE_SIZE=$((32 * K)) NUM_KEYS=$(( num_keys / 64 )) \
+ ./tools/benchmark.sh fillseq_enable_wal
+
+ # Test 2d: same as 2b, but with WAL being enabled.
+ env $ARGS ./tools/benchmark.sh fillseq_enable_wal
+
+ # Test 3: single-threaded overwrite
+ env $ARGS NUM_THREADS=1 DB_BENCH_NO_SYNC=1 ./tools/benchmark.sh overwrite
+
+else
+ echo Restoring from backup
+
+ rm -rf $db_dir
+
+ if [ ! -d ${db_dir}.bak ]; then
+ echo Database backup does not exist at ${db_dir}.bak
+ exit -1
+ fi
+
+ echo Restore database from ${db_dir}.bak
+ cp -p -r ${db_dir}.bak $db_dir
+
+ if [[ $db_dir != $wal_dir ]]; then
+ rm -rf $wal_dir
+
+ if [ ! -d ${wal_dir}.bak ]; then
+ echo WAL backup does not exist at ${wal_dir}.bak
+ exit -1
+ fi
+
+ echo Restore WAL from ${wal_dir}.bak
+ cp -p -r ${wal_dir}.bak $wal_dir
+ fi
+fi
+
+if [[ $save_setup != 0 ]]; then
+ echo Save database to ${db_dir}.bak
+ cp -p -r $db_dir ${db_dir}.bak
+
+ if [[ $db_dir != $wal_dir ]]; then
+ echo Save WAL to ${wal_dir}.bak
+ cp -p -r $wal_dir ${wal_dir}.bak
+ fi
+fi
+
+###### Read-only tests
+
+for num_thr in "${nthreads[@]}" ; do
+ # Test 4: random read
+ env $ARGS DURATION=$duration NUM_THREADS=$num_thr ./tools/benchmark.sh readrandom
+
+ # Test 5: random range scans
+ env $ARGS DURATION=$duration NUM_THREADS=$num_thr NUM_NEXTS_PER_SEEK=$nps \
+ ./tools/benchmark.sh fwdrange
+
+ # Test 6: random reverse range scans
+ env $ARGS DURATION=$duration NUM_THREADS=$num_thr NUM_NEXTS_PER_SEEK=$nps \
+ ./tools/benchmark.sh revrange
+done
+
+###### Non read-only tests
+
+for num_thr in "${nthreads[@]}" ; do
+ # Test 7: overwrite with sync=0
+ env $ARGS DURATION=$duration NUM_THREADS=$num_thr MB_WRITE_PER_SEC=$fg_mbwps \
+ DB_BENCH_NO_SYNC=1 ./tools/benchmark.sh overwrite
+
+ if [[ $skip_low_pri_tests != 1 ]]; then
+ # Test 8: overwrite with sync=1
+ env $ARGS DURATION=$duration NUM_THREADS=$num_thr MB_WRITE_PER_SEC=$fg_mbwps \
+ ./tools/benchmark.sh overwrite
+ fi
+
+ # Test 9: random update with sync=0
+ env $ARGS DURATION=$duration NUM_THREADS=$num_thr DB_BENCH_NO_SYNC=1 \
+ ./tools/benchmark.sh updaterandom
+
+ if [[ $skip_low_pri_tests != 1 ]]; then
+ # Test 10: random update with sync=1
+ env $ARGS DURATION=$duration NUM_THREADS=$num_thr ./tools/benchmark.sh updaterandom
+ fi
+
+ # Test 11: random read while writing
+ env $ARGS DURATION=$duration NUM_THREADS=$num_thr MB_WRITE_PER_SEC=$bg_mbwps \
+ DB_BENCH_NO_SYNC=1 ./tools/benchmark.sh readwhilewriting
+
+ # Test 12: range scan while writing
+ env $ARGS DURATION=$duration NUM_THREADS=$num_thr MB_WRITE_PER_SEC=$bg_mbwps \
+ DB_BENCH_NO_SYNC=1 NUM_NEXTS_PER_SEEK=$nps ./tools/benchmark.sh fwdrangewhilewriting
+
+ # Test 13: reverse range scan while writing
+ env $ARGS DURATION=$duration NUM_THREADS=$num_thr MB_WRITE_PER_SEC=$bg_mbwps \
+ DB_BENCH_NO_SYNC=1 NUM_NEXTS_PER_SEEK=$nps ./tools/benchmark.sh revrangewhilewriting
+done
+
+###### Merge tests
+
+for num_thr in "${nthreads[@]}" ; do
+ # Test 14: random merge with sync=0
+ env $ARGS DURATION=$duration NUM_THREADS=$num_thr MB_WRITE_PER_SEC=$fg_mbwps \
+ DB_BENCH_NO_SYNC=1 ./tools/benchmark.sh mergerandom
+
+ if [[ $skip_low_pri_tests != 1 ]]; then
+ # Test 15: random merge with sync=1
+ env $ARGS DURATION=$duration NUM_THREADS=$num_thr MB_WRITE_PER_SEC=$fg_mbwps \
+ ./tools/benchmark.sh mergerandom
+
+ # Test 16: random read while merging
+ env $ARGS DURATION=$duration NUM_THREADS=$num_thr MB_WRITE_PER_SEC=$bg_mbwps \
+ DB_BENCH_NO_SYNC=1 ./tools/benchmark.sh readwhilemerging
+
+ # Test 17: range scan while merging
+ env $ARGS DURATION=$duration NUM_THREADS=$num_thr MB_WRITE_PER_SEC=$bg_mbwps \
+ DB_BENCH_NO_SYNC=1 NUM_NEXTS_PER_SEEK=$nps ./tools/benchmark.sh fwdrangewhilemerging
+
+ # Test 18: reverse range scan while merging
+ env $ARGS DURATION=$duration NUM_THREADS=$num_thr MB_WRITE_PER_SEC=$bg_mbwps \
+ DB_BENCH_NO_SYNC=1 NUM_NEXTS_PER_SEEK=$nps ./tools/benchmark.sh revrangewhilemerging
+ fi
+done
+
+###### Universal compaction tests.
+
+# Use a single thread to reduce the variability in the benchmark.
+env $ARGS COMPACTION_TEST=1 NUM_THREADS=1 ./tools/benchmark.sh universal_compaction
+
+if [[ $skip_low_pri_tests != 1 ]]; then
+ echo bulkload > $output_dir/report2.txt
+ head -1 $output_dir/report.txt >> $output_dir/report2.txt
+ grep bulkload $output_dir/report.txt >> $output_dir/report2.txt
+fi
+
+echo fillseq_wal_disabled >> $output_dir/report2.txt
+head -1 $output_dir/report.txt >> $output_dir/report2.txt
+grep fillseq.wal_disabled $output_dir/report.txt >> $output_dir/report2.txt
+
+echo fillseq_wal_enabled >> $output_dir/report2.txt
+head -1 $output_dir/report.txt >> $output_dir/report2.txt
+grep fillseq.wal_enabled $output_dir/report.txt >> $output_dir/report2.txt
+
+echo overwrite sync=0 >> $output_dir/report2.txt
+head -1 $output_dir/report.txt >> $output_dir/report2.txt
+grep overwrite $output_dir/report.txt | grep \.s0 >> $output_dir/report2.txt
+
+if [[ $skip_low_pri_tests != 1 ]]; then
+ echo overwrite sync=1 >> $output_dir/report2.txt
+ head -1 $output_dir/report.txt >> $output_dir/report2.txt
+ grep overwrite $output_dir/report.txt | grep \.s1 >> $output_dir/report2.txt
+fi
+
+echo updaterandom sync=0 >> $output_dir/report2.txt
+head -1 $output_dir/report.txt >> $output_dir/report2.txt
+grep updaterandom $output_dir/report.txt | grep \.s0 >> $output_dir/report2.txt
+
+if [[ $skip_low_pri_tests != 1 ]]; then
+ echo updaterandom sync=1 >> $output_dir/report2.txt
+ head -1 $output_dir/report.txt >> $output_dir/report2.txt
+ grep updaterandom $output_dir/report.txt | grep \.s1 >> $output_dir/report2.txt
+fi
+
+echo mergerandom sync=0 >> $output_dir/report2.txt
+head -1 $output_dir/report.txt >> $output_dir/report2.txt
+grep mergerandom $output_dir/report.txt | grep \.s0 >> $output_dir/report2.txt
+
+if [[ $skip_low_pri_tests != 1 ]]; then
+ echo mergerandom sync=1 >> $output_dir/report2.txt
+ head -1 $output_dir/report.txt >> $output_dir/report2.txt
+ grep mergerandom $output_dir/report.txt | grep \.s1 >> $output_dir/report2.txt
+fi
+
+echo readrandom >> $output_dir/report2.txt
+head -1 $output_dir/report.txt >> $output_dir/report2.txt
+grep readrandom $output_dir/report.txt >> $output_dir/report2.txt
+
+echo fwdrange >> $output_dir/report2.txt
+head -1 $output_dir/report.txt >> $output_dir/report2.txt
+grep fwdrange\.t $output_dir/report.txt >> $output_dir/report2.txt
+
+echo revrange >> $output_dir/report2.txt
+head -1 $output_dir/report.txt >> $output_dir/report2.txt
+grep revrange\.t $output_dir/report.txt >> $output_dir/report2.txt
+
+echo readwhile >> $output_dir/report2.txt >> $output_dir/report2.txt
+head -1 $output_dir/report.txt >> $output_dir/report2.txt
+grep readwhilewriting $output_dir/report.txt >> $output_dir/report2.txt
+
+if [[ $skip_low_pri_tests != 1 ]]; then
+ echo readwhile >> $output_dir/report2.txt
+ head -1 $output_dir/report.txt >> $output_dir/report2.txt
+ grep readwhilemerging $output_dir/report.txt >> $output_dir/report2.txt
+fi
+
+echo fwdreadwhilewriting >> $output_dir/report2.txt
+head -1 $output_dir/report.txt >> $output_dir/report2.txt
+grep fwdrangewhilewriting $output_dir/report.txt >> $output_dir/report2.txt
+
+if [[ $skip_low_pri_tests != 1 ]]; then
+ echo fwdreadwhilemerging >> $output_dir/report2.txt
+ head -1 $output_dir/report.txt >> $output_dir/report2.txt
+ grep fwdrangewhilemerg $output_dir/report.txt >> $output_dir/report2.txt
+fi
+
+echo revreadwhilewriting >> $output_dir/report2.txt
+head -1 $output_dir/report.txt >> $output_dir/report2.txt
+grep revrangewhilewriting $output_dir/report.txt >> $output_dir/report2.txt
+
+if [[ $skip_low_pri_tests != 1 ]]; then
+ echo revreadwhilemerging >> $output_dir/report2.txt
+ head -1 $output_dir/report.txt >> $output_dir/report2.txt
+ grep revrangewhilemerg $output_dir/report.txt >> $output_dir/report2.txt
+fi
+
+cat $output_dir/report2.txt
diff --git a/src/rocksdb/tools/run_leveldb.sh b/src/rocksdb/tools/run_leveldb.sh
new file mode 100755
index 000000000..2fce8b12d
--- /dev/null
+++ b/src/rocksdb/tools/run_leveldb.sh
@@ -0,0 +1,175 @@
+#!/usr/bin/env bash
+# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
+# REQUIRE: benchmark_leveldb.sh exists in the current directory
+# After execution of this script, log files are generated in $output_dir.
+# report.txt provides a high level statistics
+#
+# This should be used with the LevelDB fork listed here to use additional test options.
+# For more details on the changes see the blog post listed below.
+# https://github.com/mdcallag/leveldb-1
+# http://smalldatum.blogspot.com/2015/04/comparing-leveldb-and-rocksdb-take-2.html
+#
+# This should be run from the parent of the tools directory. The command line is:
+# [$env_vars] tools/run_flash_bench.sh [list-of-threads]
+#
+# This runs a sequence of tests in the following sequence:
+# step 1) load - bulkload, compact, fillseq, overwrite
+# step 2) read-only for each number of threads
+# step 3) read-write for each number of threads
+#
+# The list of threads is optional and when not set is equivalent to "24".
+# Were list-of-threads specified as "1 2 4" then the tests in steps 2, 3 and
+# 4 above would be repeated for 1, 2 and 4 threads. The tests in step 1 are
+# only run for 1 thread.
+
+# Test output is written to $OUTPUT_DIR, currently /tmp/output. The performance
+# summary is in $OUTPUT_DIR/report.txt. There is one file in $OUTPUT_DIR per
+# test and the tests are listed below.
+#
+# The environment variables are also optional. The variables are:
+# NKEYS - number of key/value pairs to load
+# NWRITESPERSEC - the writes/second rate limit for the *whilewriting* tests.
+# If this is too large then the non-writer threads can get
+# starved.
+# VAL_SIZE - the length of the value in the key/value pairs loaded.
+# You can estimate the size of the test database from this,
+# NKEYS and the compression rate (--compression_ratio) set
+# in tools/benchmark_leveldb.sh
+# BLOCK_LENGTH - value for db_bench --block_size
+# CACHE_BYTES - the size of the RocksDB block cache in bytes
+# DATA_DIR - directory in which to create database files
+# DO_SETUP - when set to 0 then a backup of the database is copied from
+# $DATA_DIR.bak to $DATA_DIR and the load tests from step 1
+# This allows tests from steps 2, 3 to be repeated faster.
+# SAVE_SETUP - saves a copy of the database at the end of step 1 to
+# $DATA_DIR.bak.
+
+# Size constants
+K=1024
+M=$((1024 * K))
+G=$((1024 * M))
+
+num_keys=${NKEYS:-$((1 * G))}
+wps=${NWRITESPERSEC:-$((10 * K))}
+vs=${VAL_SIZE:-400}
+cs=${CACHE_BYTES:-$(( 1 * G ))}
+bs=${BLOCK_LENGTH:-4096}
+
+# If no command line arguments then run for 24 threads.
+if [[ $# -eq 0 ]]; then
+ nthreads=( 24 )
+else
+ nthreads=( "$@" )
+fi
+
+for num_thr in "${nthreads[@]}" ; do
+ echo Will run for $num_thr threads
+done
+
+# Update these parameters before execution !!!
+db_dir=${DATA_DIR:-"/tmp/rocksdb/"}
+
+do_setup=${DO_SETUP:-1}
+save_setup=${SAVE_SETUP:-0}
+
+output_dir="${TMPDIR:-/tmp}/output"
+
+ARGS="\
+OUTPUT_DIR=$output_dir \
+NUM_KEYS=$num_keys \
+DB_DIR=$db_dir \
+VALUE_SIZE=$vs \
+BLOCK_SIZE=$bs \
+CACHE_SIZE=$cs"
+
+mkdir -p $output_dir
+echo -e "ops/sec\tmb/sec\tusec/op\tavg\tp50\tTest" \
+ > $output_dir/report.txt
+
+# Notes on test sequence:
+# step 1) Setup database via sequential fill followed by overwrite to fragment it.
+# Done without setting DURATION to make sure that overwrite does $num_keys writes
+# step 2) read-only tests for all levels of concurrency requested
+# step 3) non read-only tests for all levels of concurrency requested
+
+###### Setup the database
+
+if [[ $do_setup != 0 ]]; then
+ echo Doing setup
+
+ # Test 2a: sequential fill with large values to get peak ingest
+ # adjust NUM_KEYS given the use of larger values
+ env $ARGS BLOCK_SIZE=$((1 * M)) VALUE_SIZE=$((32 * K)) NUM_KEYS=$(( num_keys / 64 )) \
+ ./tools/benchmark_leveldb.sh fillseq
+
+ # Test 2b: sequential fill with the configured value size
+ env $ARGS ./tools/benchmark_leveldb.sh fillseq
+
+ # Test 3: single-threaded overwrite
+ env $ARGS NUM_THREADS=1 DB_BENCH_NO_SYNC=1 ./tools/benchmark_leveldb.sh overwrite
+
+else
+ echo Restoring from backup
+
+ rm -rf $db_dir
+
+ if [ ! -d ${db_dir}.bak ]; then
+ echo Database backup does not exist at ${db_dir}.bak
+ exit -1
+ fi
+
+ echo Restore database from ${db_dir}.bak
+ cp -p -r ${db_dir}.bak $db_dir
+fi
+
+if [[ $save_setup != 0 ]]; then
+ echo Save database to ${db_dir}.bak
+ cp -p -r $db_dir ${db_dir}.bak
+fi
+
+###### Read-only tests
+
+for num_thr in "${nthreads[@]}" ; do
+ # Test 4: random read
+ env $ARGS NUM_THREADS=$num_thr ./tools/benchmark_leveldb.sh readrandom
+
+done
+
+###### Non read-only tests
+
+for num_thr in "${nthreads[@]}" ; do
+ # Test 7: overwrite with sync=0
+ env $ARGS NUM_THREADS=$num_thr DB_BENCH_NO_SYNC=1 \
+ ./tools/benchmark_leveldb.sh overwrite
+
+ # Test 8: overwrite with sync=1
+ # Not run for now because LevelDB db_bench doesn't have an option to limit the
+ # test run to X seconds and doing sync-per-commit for --num can take too long.
+ # env $ARGS NUM_THREADS=$num_thr ./tools/benchmark_leveldb.sh overwrite
+
+ # Test 11: random read while writing
+ env $ARGS NUM_THREADS=$num_thr WRITES_PER_SECOND=$wps \
+ ./tools/benchmark_leveldb.sh readwhilewriting
+
+done
+
+echo bulkload > $output_dir/report2.txt
+head -1 $output_dir/report.txt >> $output_dir/report2.txt
+grep bulkload $output_dir/report.txt >> $output_dir/report2.txt
+echo fillseq >> $output_dir/report2.txt
+head -1 $output_dir/report.txt >> $output_dir/report2.txt
+grep fillseq $output_dir/report.txt >> $output_dir/report2.txt
+echo overwrite sync=0 >> $output_dir/report2.txt
+head -1 $output_dir/report.txt >> $output_dir/report2.txt
+grep overwrite $output_dir/report.txt | grep \.s0 >> $output_dir/report2.txt
+echo overwrite sync=1 >> $output_dir/report2.txt
+head -1 $output_dir/report.txt >> $output_dir/report2.txt
+grep overwrite $output_dir/report.txt | grep \.s1 >> $output_dir/report2.txt
+echo readrandom >> $output_dir/report2.txt
+head -1 $output_dir/report.txt >> $output_dir/report2.txt
+grep readrandom $output_dir/report.txt >> $output_dir/report2.txt
+echo readwhile >> $output_dir/report2.txt >> $output_dir/report2.txt
+head -1 $output_dir/report.txt >> $output_dir/report2.txt
+grep readwhilewriting $output_dir/report.txt >> $output_dir/report2.txt
+
+cat $output_dir/report2.txt
diff --git a/src/rocksdb/tools/sample-dump.dmp b/src/rocksdb/tools/sample-dump.dmp
new file mode 100644
index 000000000..4ec3a7732
--- /dev/null
+++ b/src/rocksdb/tools/sample-dump.dmp
Binary files differ
diff --git a/src/rocksdb/tools/simulated_hybrid_file_system.cc b/src/rocksdb/tools/simulated_hybrid_file_system.cc
new file mode 100644
index 000000000..a474417c7
--- /dev/null
+++ b/src/rocksdb/tools/simulated_hybrid_file_system.cc
@@ -0,0 +1,245 @@
+// Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
+// This source code is licensed under both the GPLv2 (found in the
+// COPYING file in the root directory) and Apache 2.0 License
+// (found in the LICENSE.Apache file in the root directory).
+
+#include "util/stop_watch.h"
+#ifndef ROCKSDB_LITE
+
+#include <algorithm>
+#include <sstream>
+#include <string>
+
+#include "rocksdb/rate_limiter.h"
+#include "tools/simulated_hybrid_file_system.h"
+
+namespace ROCKSDB_NAMESPACE {
+
+const int64_t kUsPerSec = 1000000;
+const int64_t kDummyBytesPerUs = 1024;
+
+namespace {
+// From bytes to read/write, calculate service time needed by an HDD.
+// This is used to simulate latency from HDD.
+int CalculateServeTimeUs(size_t bytes) {
+ return 12200 + static_cast<int>(static_cast<double>(bytes) * 0.005215);
+}
+
+// There is a bug in rater limiter that would crash with small requests
+// Hack to get it around.
+void RateLimiterRequest(RateLimiter* rater_limiter, int64_t amount) {
+ int64_t left = amount * kDummyBytesPerUs;
+ const int64_t kMaxToRequest = kDummyBytesPerUs * kUsPerSec / 1024;
+ while (left > 0) {
+ int64_t to_request = std::min(kMaxToRequest, left);
+ rater_limiter->Request(to_request, Env::IOPriority::IO_LOW, nullptr);
+ left -= to_request;
+ }
+}
+} // namespace
+
+// The metadata file format: each line is a full filename of a file which is
+// warm
+SimulatedHybridFileSystem::SimulatedHybridFileSystem(
+ const std::shared_ptr<FileSystem>& base,
+ const std::string& metadata_file_name, int throughput_multiplier,
+ bool is_full_fs_warm)
+ : FileSystemWrapper(base),
+ // Limit to 100 requests per second.
+ rate_limiter_(NewGenericRateLimiter(
+ int64_t{throughput_multiplier} * kDummyBytesPerUs *
+ kUsPerSec /* rate_bytes_per_sec */,
+ 1000 /* refill_period_us */)),
+ metadata_file_name_(metadata_file_name),
+ name_("SimulatedHybridFileSystem: " + std::string(target()->Name())),
+ is_full_fs_warm_(is_full_fs_warm) {
+ IOStatus s = base->FileExists(metadata_file_name, IOOptions(), nullptr);
+ if (s.IsNotFound()) {
+ return;
+ }
+ std::string metadata;
+ s = ReadFileToString(base.get(), metadata_file_name, &metadata);
+ if (!s.ok()) {
+ fprintf(stderr, "Error reading from file %s: %s",
+ metadata_file_name.c_str(), s.ToString().c_str());
+ // Exit rather than assert as this file system is built to run with
+ // benchmarks, which usually run on release mode.
+ std::exit(1);
+ }
+ std::istringstream input;
+ input.str(metadata);
+ std::string line;
+ while (std::getline(input, line)) {
+ fprintf(stderr, "Warm file %s\n", line.c_str());
+ warm_file_set_.insert(line);
+ }
+}
+
+// Need to write out the metadata file to file. See comment of
+// SimulatedHybridFileSystem::SimulatedHybridFileSystem() for format of the
+// file.
+SimulatedHybridFileSystem::~SimulatedHybridFileSystem() {
+ if (metadata_file_name_.empty()) {
+ return;
+ }
+ std::string metadata;
+ for (const auto& f : warm_file_set_) {
+ metadata += f;
+ metadata += "\n";
+ }
+ IOStatus s = WriteStringToFile(target(), metadata, metadata_file_name_, true);
+ if (!s.ok()) {
+ fprintf(stderr, "Error writing to file %s: %s", metadata_file_name_.c_str(),
+ s.ToString().c_str());
+ }
+}
+
+IOStatus SimulatedHybridFileSystem::NewRandomAccessFile(
+ const std::string& fname, const FileOptions& file_opts,
+ std::unique_ptr<FSRandomAccessFile>* result, IODebugContext* dbg) {
+ Temperature temperature = Temperature::kUnknown;
+ if (is_full_fs_warm_) {
+ temperature = Temperature::kWarm;
+ } else {
+ const std::lock_guard<std::mutex> lock(mutex_);
+ if (warm_file_set_.find(fname) != warm_file_set_.end()) {
+ temperature = Temperature::kWarm;
+ }
+ assert(temperature == file_opts.temperature);
+ }
+ IOStatus s = target()->NewRandomAccessFile(fname, file_opts, result, dbg);
+ result->reset(
+ new SimulatedHybridRaf(std::move(*result), rate_limiter_, temperature));
+ return s;
+}
+
+IOStatus SimulatedHybridFileSystem::NewWritableFile(
+ const std::string& fname, const FileOptions& file_opts,
+ std::unique_ptr<FSWritableFile>* result, IODebugContext* dbg) {
+ if (file_opts.temperature == Temperature::kWarm) {
+ const std::lock_guard<std::mutex> lock(mutex_);
+ warm_file_set_.insert(fname);
+ }
+
+ IOStatus s = target()->NewWritableFile(fname, file_opts, result, dbg);
+ if (file_opts.temperature == Temperature::kWarm || is_full_fs_warm_) {
+ result->reset(new SimulatedWritableFile(std::move(*result), rate_limiter_));
+ }
+ return s;
+}
+
+IOStatus SimulatedHybridFileSystem::DeleteFile(const std::string& fname,
+ const IOOptions& options,
+ IODebugContext* dbg) {
+ {
+ const std::lock_guard<std::mutex> lock(mutex_);
+ warm_file_set_.erase(fname);
+ }
+ return target()->DeleteFile(fname, options, dbg);
+}
+
+IOStatus SimulatedHybridRaf::Read(uint64_t offset, size_t n,
+ const IOOptions& options, Slice* result,
+ char* scratch, IODebugContext* dbg) const {
+ if (temperature_ == Temperature::kWarm) {
+ SimulateIOWait(n);
+ }
+ return target()->Read(offset, n, options, result, scratch, dbg);
+}
+
+IOStatus SimulatedHybridRaf::MultiRead(FSReadRequest* reqs, size_t num_reqs,
+ const IOOptions& options,
+ IODebugContext* dbg) {
+ if (temperature_ == Temperature::kWarm) {
+ for (size_t i = 0; i < num_reqs; i++) {
+ SimulateIOWait(reqs[i].len);
+ }
+ }
+ return target()->MultiRead(reqs, num_reqs, options, dbg);
+}
+
+IOStatus SimulatedHybridRaf::Prefetch(uint64_t offset, size_t n,
+ const IOOptions& options,
+ IODebugContext* dbg) {
+ if (temperature_ == Temperature::kWarm) {
+ SimulateIOWait(n);
+ }
+ return target()->Prefetch(offset, n, options, dbg);
+}
+
+void SimulatedHybridRaf::SimulateIOWait(int64_t bytes) const {
+ int serve_time = CalculateServeTimeUs(bytes);
+ {
+ StopWatchNano stop_watch(Env::Default()->GetSystemClock().get(),
+ /*auto_start=*/true);
+ RateLimiterRequest(rate_limiter_.get(), serve_time);
+ int time_passed_us = static_cast<int>(stop_watch.ElapsedNanos() / 1000);
+ if (time_passed_us < serve_time) {
+ Env::Default()->SleepForMicroseconds(serve_time - time_passed_us);
+ }
+ }
+}
+
+void SimulatedWritableFile::SimulateIOWait(int64_t bytes) const {
+ int serve_time = CalculateServeTimeUs(bytes);
+ Env::Default()->SleepForMicroseconds(serve_time);
+ RateLimiterRequest(rate_limiter_.get(), serve_time);
+}
+
+IOStatus SimulatedWritableFile::Append(const Slice& data, const IOOptions& ioo,
+ IODebugContext* idc) {
+ if (use_direct_io()) {
+ SimulateIOWait(data.size());
+ } else {
+ unsynced_bytes += data.size();
+ }
+ return target()->Append(data, ioo, idc);
+}
+
+IOStatus SimulatedWritableFile::Append(
+ const Slice& data, const IOOptions& options,
+ const DataVerificationInfo& verification_info, IODebugContext* dbg) {
+ if (use_direct_io()) {
+ SimulateIOWait(data.size());
+ } else {
+ unsynced_bytes += data.size();
+ }
+ return target()->Append(data, options, verification_info, dbg);
+}
+
+IOStatus SimulatedWritableFile::PositionedAppend(const Slice& data,
+ uint64_t offset,
+ const IOOptions& options,
+ IODebugContext* dbg) {
+ if (use_direct_io()) {
+ SimulateIOWait(data.size());
+ } else {
+ // This might be overcalculated, but it's probably OK.
+ unsynced_bytes += data.size();
+ }
+ return target()->PositionedAppend(data, offset, options, dbg);
+}
+IOStatus SimulatedWritableFile::PositionedAppend(
+ const Slice& data, uint64_t offset, const IOOptions& options,
+ const DataVerificationInfo& verification_info, IODebugContext* dbg) {
+ if (use_direct_io()) {
+ SimulateIOWait(data.size());
+ } else {
+ // This might be overcalculated, but it's probably OK.
+ unsynced_bytes += data.size();
+ }
+ return target()->PositionedAppend(data, offset, options, verification_info,
+ dbg);
+}
+
+IOStatus SimulatedWritableFile::Sync(const IOOptions& options,
+ IODebugContext* dbg) {
+ if (unsynced_bytes > 0) {
+ SimulateIOWait(unsynced_bytes);
+ unsynced_bytes = 0;
+ }
+ return target()->Sync(options, dbg);
+}
+} // namespace ROCKSDB_NAMESPACE
+
+#endif // ROCKSDB_LITE
diff --git a/src/rocksdb/tools/simulated_hybrid_file_system.h b/src/rocksdb/tools/simulated_hybrid_file_system.h
new file mode 100644
index 000000000..251d89df7
--- /dev/null
+++ b/src/rocksdb/tools/simulated_hybrid_file_system.h
@@ -0,0 +1,126 @@
+// Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
+// This source code is licensed under both the GPLv2 (found in the
+// COPYING file in the root directory) and Apache 2.0 License
+// (found in the LICENSE.Apache file in the root directory).
+
+#pragma once
+
+#ifndef ROCKSDB_LITE
+
+#include <utility>
+
+#include "rocksdb/file_system.h"
+
+namespace ROCKSDB_NAMESPACE {
+
+// A FileSystem simulates hybrid file system by ingesting latency and limit
+// IOPs.
+// This class is only used for development purpose and should not be used
+// in production.
+// Right now we ingest 15ms latency and allow 100 requests per second when
+// the file is for warm temperature.
+// When the object is destroyed, the list of warm files are written to a
+// file, which can be used to reopen a FileSystem and still recover the
+// list. This is to allow the information to preserve between db_bench
+// runs.
+class SimulatedHybridFileSystem : public FileSystemWrapper {
+ public:
+ // metadata_file_name stores metadata of the files, so that it can be
+ // loaded after process restarts. If the file doesn't exist, create
+ // one. The file is written when the class is destroyed.
+ // throughput_multiplier: multiplier of throughput. For example, 1 is to
+ // simulate single disk spindle. 4 is to simualte 4 disk spindles.
+ // is_full_fs_warm: if true, all files are all included in slow I/O
+ // simulation.
+ SimulatedHybridFileSystem(const std::shared_ptr<FileSystem>& base,
+ const std::string& metadata_file_name,
+ int throughput_multiplier, bool is_full_fs_warm);
+
+ ~SimulatedHybridFileSystem() override;
+
+ public:
+ IOStatus NewRandomAccessFile(const std::string& fname,
+ const FileOptions& file_opts,
+ std::unique_ptr<FSRandomAccessFile>* result,
+ IODebugContext* dbg) override;
+ IOStatus NewWritableFile(const std::string& fname,
+ const FileOptions& file_opts,
+ std::unique_ptr<FSWritableFile>* result,
+ IODebugContext* dbg) override;
+ IOStatus DeleteFile(const std::string& fname, const IOOptions& options,
+ IODebugContext* dbg) override;
+
+ const char* Name() const override { return name_.c_str(); }
+
+ private:
+ // Limit 100 requests per second. Rate limiter is designed to byte but
+ // we use it as fixed bytes is one request.
+ std::shared_ptr<RateLimiter> rate_limiter_;
+ std::mutex mutex_;
+ std::unordered_set<std::string> warm_file_set_;
+ std::string metadata_file_name_;
+ std::string name_;
+ bool is_full_fs_warm_;
+};
+
+// Simulated random access file that can control IOPs and latency to simulate
+// specific storage media
+class SimulatedHybridRaf : public FSRandomAccessFileOwnerWrapper {
+ public:
+ SimulatedHybridRaf(std::unique_ptr<FSRandomAccessFile>&& t,
+ std::shared_ptr<RateLimiter> rate_limiter,
+ Temperature temperature)
+ : FSRandomAccessFileOwnerWrapper(std::move(t)),
+ rate_limiter_(rate_limiter),
+ temperature_(temperature) {}
+
+ ~SimulatedHybridRaf() override {}
+
+ IOStatus Read(uint64_t offset, size_t n, const IOOptions& options,
+ Slice* result, char* scratch,
+ IODebugContext* dbg) const override;
+
+ IOStatus MultiRead(FSReadRequest* reqs, size_t num_reqs,
+ const IOOptions& options, IODebugContext* dbg) override;
+
+ IOStatus Prefetch(uint64_t offset, size_t n, const IOOptions& options,
+ IODebugContext* dbg) override;
+
+ private:
+ std::shared_ptr<RateLimiter> rate_limiter_;
+ Temperature temperature_;
+
+ void SimulateIOWait(int64_t num_requests) const;
+};
+
+class SimulatedWritableFile : public FSWritableFileWrapper {
+ public:
+ SimulatedWritableFile(std::unique_ptr<FSWritableFile>&& t,
+ std::shared_ptr<RateLimiter> rate_limiter)
+ : FSWritableFileWrapper(t.get()),
+ file_guard_(std::move(t)),
+ rate_limiter_(rate_limiter) {}
+ IOStatus Append(const Slice& data, const IOOptions&,
+ IODebugContext*) override;
+ IOStatus Append(const Slice& data, const IOOptions& options,
+ const DataVerificationInfo& verification_info,
+ IODebugContext* dbg) override;
+ IOStatus Sync(const IOOptions& options, IODebugContext* dbg) override;
+ IOStatus PositionedAppend(const Slice& data, uint64_t offset,
+ const IOOptions& options,
+ IODebugContext* dbg) override;
+ IOStatus PositionedAppend(const Slice& data, uint64_t offset,
+ const IOOptions& options,
+ const DataVerificationInfo& verification_info,
+ IODebugContext* dbg) override;
+
+ private:
+ std::unique_ptr<FSWritableFile> file_guard_;
+ std::shared_ptr<RateLimiter> rate_limiter_;
+ size_t unsynced_bytes = 0;
+
+ void SimulateIOWait(int64_t num_requests) const;
+};
+} // namespace ROCKSDB_NAMESPACE
+
+#endif // ROCKSDB_LITE
diff --git a/src/rocksdb/tools/sst_dump.cc b/src/rocksdb/tools/sst_dump.cc
new file mode 100644
index 000000000..becf67316
--- /dev/null
+++ b/src/rocksdb/tools/sst_dump.cc
@@ -0,0 +1,20 @@
+// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+// This source code is licensed under both the GPLv2 (found in the
+// COPYING file in the root directory) and Apache 2.0 License
+// (found in the LICENSE.Apache file in the root directory).
+//
+#ifndef ROCKSDB_LITE
+
+#include "rocksdb/sst_dump_tool.h"
+
+int main(int argc, char** argv) {
+ ROCKSDB_NAMESPACE::SSTDumpTool tool;
+ return tool.Run(argc, argv);
+}
+#else
+#include <stdio.h>
+int main(int /*argc*/, char** /*argv*/) {
+ fprintf(stderr, "Not supported in lite mode.\n");
+ return 1;
+}
+#endif // ROCKSDB_LITE
diff --git a/src/rocksdb/tools/sst_dump_test.cc b/src/rocksdb/tools/sst_dump_test.cc
new file mode 100644
index 000000000..aa1ff810f
--- /dev/null
+++ b/src/rocksdb/tools/sst_dump_test.cc
@@ -0,0 +1,481 @@
+// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+// This source code is licensed under both the GPLv2 (found in the
+// COPYING file in the root directory) and Apache 2.0 License
+// (found in the LICENSE.Apache file in the root directory).
+//
+// Copyright (c) 2012 The LevelDB Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file. See the AUTHORS file for names of contributors.
+#ifndef ROCKSDB_LITE
+
+#include <stdint.h>
+
+#include "file/random_access_file_reader.h"
+#include "port/stack_trace.h"
+#include "rocksdb/convenience.h"
+#include "rocksdb/filter_policy.h"
+#include "rocksdb/sst_dump_tool.h"
+#include "table/block_based/block_based_table_factory.h"
+#include "table/table_builder.h"
+#include "test_util/testharness.h"
+#include "test_util/testutil.h"
+
+namespace ROCKSDB_NAMESPACE {
+
+const uint32_t kOptLength = 1024;
+
+namespace {
+static std::string MakeKey(int i) {
+ char buf[100];
+ snprintf(buf, sizeof(buf), "k_%04d", i);
+ InternalKey key(std::string(buf), 0, ValueType::kTypeValue);
+ return key.Encode().ToString();
+}
+
+static std::string MakeKeyWithTimeStamp(int i, uint64_t ts) {
+ char buf[100];
+ snprintf(buf, sizeof(buf), "k_%04d", i);
+ return test::KeyStr(ts, std::string(buf), /*seq=*/0, kTypeValue);
+}
+
+static std::string MakeValue(int i) {
+ char buf[100];
+ snprintf(buf, sizeof(buf), "v_%04d", i);
+ InternalKey key(std::string(buf), 0, ValueType::kTypeValue);
+ return key.Encode().ToString();
+}
+
+void cleanup(const Options& opts, const std::string& file_name) {
+ Env* env = opts.env;
+ ASSERT_OK(env->DeleteFile(file_name));
+ std::string outfile_name = file_name.substr(0, file_name.length() - 4);
+ outfile_name.append("_dump.txt");
+ env->DeleteFile(outfile_name).PermitUncheckedError();
+}
+} // namespace
+
+// Test for sst dump tool "raw" mode
+class SSTDumpToolTest : public testing::Test {
+ std::string test_dir_;
+ Env* env_;
+ std::shared_ptr<Env> env_guard_;
+
+ public:
+ SSTDumpToolTest() : env_(Env::Default()) {
+ EXPECT_OK(test::CreateEnvFromSystem(ConfigOptions(), &env_, &env_guard_));
+ test_dir_ = test::PerThreadDBPath(env_, "sst_dump_test_db");
+ Status s = env_->CreateDirIfMissing(test_dir_);
+ EXPECT_OK(s);
+ }
+
+ ~SSTDumpToolTest() override {
+ if (getenv("KEEP_DB")) {
+ fprintf(stdout, "Data is still at %s\n", test_dir_.c_str());
+ } else {
+ EXPECT_OK(env_->DeleteDir(test_dir_));
+ }
+ }
+
+ Env* env() { return env_; }
+
+ std::string MakeFilePath(const std::string& file_name) const {
+ std::string path(test_dir_);
+ path.append("/").append(file_name);
+ return path;
+ }
+
+ template <std::size_t N>
+ void PopulateCommandArgs(const std::string& file_path, const char* command,
+ char* (&usage)[N]) const {
+ for (int i = 0; i < static_cast<int>(N); ++i) {
+ usage[i] = new char[kOptLength];
+ }
+ snprintf(usage[0], kOptLength, "./sst_dump");
+ snprintf(usage[1], kOptLength, "%s", command);
+ snprintf(usage[2], kOptLength, "--file=%s", file_path.c_str());
+ }
+
+ void createSST(const Options& opts, const std::string& file_name) {
+ Env* test_env = opts.env;
+ FileOptions file_options(opts);
+ ReadOptions read_options;
+ const ImmutableOptions imoptions(opts);
+ const MutableCFOptions moptions(opts);
+ ROCKSDB_NAMESPACE::InternalKeyComparator ikc(opts.comparator);
+ std::unique_ptr<TableBuilder> tb;
+
+ IntTblPropCollectorFactories int_tbl_prop_collector_factories;
+ std::unique_ptr<WritableFileWriter> file_writer;
+ ASSERT_OK(WritableFileWriter::Create(test_env->GetFileSystem(), file_name,
+ file_options, &file_writer, nullptr));
+
+ std::string column_family_name;
+ int unknown_level = -1;
+ tb.reset(opts.table_factory->NewTableBuilder(
+ TableBuilderOptions(
+ imoptions, moptions, ikc, &int_tbl_prop_collector_factories,
+ CompressionType::kNoCompression, CompressionOptions(),
+ TablePropertiesCollectorFactory::Context::kUnknownColumnFamily,
+ column_family_name, unknown_level),
+ file_writer.get()));
+
+ // Populate slightly more than 1K keys
+ uint32_t num_keys = kNumKey;
+ const char* comparator_name = ikc.user_comparator()->Name();
+ if (strcmp(comparator_name, ReverseBytewiseComparator()->Name()) == 0) {
+ for (int32_t i = num_keys; i >= 0; i--) {
+ tb->Add(MakeKey(i), MakeValue(i));
+ }
+ } else if (strcmp(comparator_name,
+ test::BytewiseComparatorWithU64TsWrapper()->Name()) ==
+ 0) {
+ for (uint32_t i = 0; i < num_keys; i++) {
+ tb->Add(MakeKeyWithTimeStamp(i, 100 + i), MakeValue(i));
+ }
+ } else {
+ for (uint32_t i = 0; i < num_keys; i++) {
+ tb->Add(MakeKey(i), MakeValue(i));
+ }
+ }
+ ASSERT_OK(tb->Finish());
+ ASSERT_OK(file_writer->Close());
+ }
+
+ protected:
+ constexpr static int kNumKey = 1024;
+};
+
+constexpr int SSTDumpToolTest::kNumKey;
+
+TEST_F(SSTDumpToolTest, HelpAndVersion) {
+ Options opts;
+ opts.env = env();
+
+ ROCKSDB_NAMESPACE::SSTDumpTool tool;
+
+ static const char* help[] = {"./sst_dump", "--help"};
+ ASSERT_TRUE(!tool.Run(2, help, opts));
+ static const char* version[] = {"./sst_dump", "--version"};
+ ASSERT_TRUE(!tool.Run(2, version, opts));
+ static const char* bad[] = {"./sst_dump", "--not_an_option"};
+ ASSERT_TRUE(tool.Run(2, bad, opts));
+}
+
+TEST_F(SSTDumpToolTest, EmptyFilter) {
+ Options opts;
+ opts.env = env();
+ std::string file_path = MakeFilePath("rocksdb_sst_test.sst");
+ createSST(opts, file_path);
+
+ char* usage[3];
+ PopulateCommandArgs(file_path, "--command=raw", usage);
+
+ ROCKSDB_NAMESPACE::SSTDumpTool tool;
+ ASSERT_TRUE(!tool.Run(3, usage, opts));
+
+ cleanup(opts, file_path);
+ for (int i = 0; i < 3; i++) {
+ delete[] usage[i];
+ }
+}
+
+TEST_F(SSTDumpToolTest, SstDumpReverseBytewiseComparator) {
+ Options opts;
+ opts.env = env();
+ opts.comparator = ReverseBytewiseComparator();
+ BlockBasedTableOptions table_opts;
+ table_opts.filter_policy.reset(
+ ROCKSDB_NAMESPACE::NewBloomFilterPolicy(10, false));
+ opts.table_factory.reset(new BlockBasedTableFactory(table_opts));
+ std::string file_path =
+ MakeFilePath("rocksdb_sst_reverse_bytewise_comparator.sst");
+ createSST(opts, file_path);
+
+ char* usage[3];
+ PopulateCommandArgs(file_path, "--command=raw", usage);
+
+ ROCKSDB_NAMESPACE::SSTDumpTool tool;
+ ASSERT_TRUE(!tool.Run(3, usage, opts));
+
+ cleanup(opts, file_path);
+ for (int i = 0; i < 3; i++) {
+ delete[] usage[i];
+ }
+}
+
+TEST_F(SSTDumpToolTest, SstDumpComparatorWithU64Ts) {
+ Options opts;
+ opts.env = env();
+ opts.comparator = test::BytewiseComparatorWithU64TsWrapper();
+ BlockBasedTableOptions table_opts;
+ table_opts.filter_policy.reset(
+ ROCKSDB_NAMESPACE::NewBloomFilterPolicy(10, false));
+ opts.table_factory.reset(new BlockBasedTableFactory(table_opts));
+ std::string file_path =
+ MakeFilePath("rocksdb_sst_comparator_with_u64_ts.sst");
+ createSST(opts, file_path);
+
+ char* usage[3];
+ PopulateCommandArgs(file_path, "--command=raw", usage);
+
+ ROCKSDB_NAMESPACE::SSTDumpTool tool;
+ ASSERT_TRUE(!tool.Run(3, usage, opts));
+
+ cleanup(opts, file_path);
+ for (int i = 0; i < 3; i++) {
+ delete[] usage[i];
+ }
+}
+
+TEST_F(SSTDumpToolTest, FilterBlock) {
+ Options opts;
+ opts.env = env();
+ BlockBasedTableOptions table_opts;
+ table_opts.filter_policy.reset(
+ ROCKSDB_NAMESPACE::NewBloomFilterPolicy(10, true));
+ opts.table_factory.reset(new BlockBasedTableFactory(table_opts));
+ std::string file_path = MakeFilePath("rocksdb_sst_test.sst");
+ createSST(opts, file_path);
+
+ char* usage[3];
+ PopulateCommandArgs(file_path, "--command=raw", usage);
+
+ ROCKSDB_NAMESPACE::SSTDumpTool tool;
+ ASSERT_TRUE(!tool.Run(3, usage, opts));
+
+ cleanup(opts, file_path);
+ for (int i = 0; i < 3; i++) {
+ delete[] usage[i];
+ }
+}
+
+TEST_F(SSTDumpToolTest, FullFilterBlock) {
+ Options opts;
+ opts.env = env();
+ BlockBasedTableOptions table_opts;
+ table_opts.filter_policy.reset(
+ ROCKSDB_NAMESPACE::NewBloomFilterPolicy(10, false));
+ opts.table_factory.reset(new BlockBasedTableFactory(table_opts));
+ std::string file_path = MakeFilePath("rocksdb_sst_test.sst");
+ createSST(opts, file_path);
+
+ char* usage[3];
+ PopulateCommandArgs(file_path, "--command=raw", usage);
+
+ ROCKSDB_NAMESPACE::SSTDumpTool tool;
+ ASSERT_TRUE(!tool.Run(3, usage, opts));
+
+ cleanup(opts, file_path);
+ for (int i = 0; i < 3; i++) {
+ delete[] usage[i];
+ }
+}
+
+TEST_F(SSTDumpToolTest, GetProperties) {
+ Options opts;
+ opts.env = env();
+ BlockBasedTableOptions table_opts;
+ table_opts.filter_policy.reset(
+ ROCKSDB_NAMESPACE::NewBloomFilterPolicy(10, false));
+ opts.table_factory.reset(new BlockBasedTableFactory(table_opts));
+ std::string file_path = MakeFilePath("rocksdb_sst_test.sst");
+ createSST(opts, file_path);
+
+ char* usage[3];
+ PopulateCommandArgs(file_path, "--show_properties", usage);
+
+ ROCKSDB_NAMESPACE::SSTDumpTool tool;
+ ASSERT_TRUE(!tool.Run(3, usage, opts));
+
+ cleanup(opts, file_path);
+ for (int i = 0; i < 3; i++) {
+ delete[] usage[i];
+ }
+}
+
+TEST_F(SSTDumpToolTest, CompressedSizes) {
+ Options opts;
+ opts.env = env();
+ BlockBasedTableOptions table_opts;
+ table_opts.filter_policy.reset(
+ ROCKSDB_NAMESPACE::NewBloomFilterPolicy(10, false));
+ opts.table_factory.reset(new BlockBasedTableFactory(table_opts));
+ std::string file_path = MakeFilePath("rocksdb_sst_test.sst");
+ createSST(opts, file_path);
+
+ char* usage[3];
+ PopulateCommandArgs(file_path, "--command=recompress", usage);
+
+ ROCKSDB_NAMESPACE::SSTDumpTool tool;
+ ASSERT_TRUE(!tool.Run(3, usage, opts));
+
+ cleanup(opts, file_path);
+ for (int i = 0; i < 3; i++) {
+ delete[] usage[i];
+ }
+}
+
+TEST_F(SSTDumpToolTest, MemEnv) {
+ std::unique_ptr<Env> mem_env(NewMemEnv(env()));
+ Options opts;
+ opts.env = mem_env.get();
+ std::string file_path = MakeFilePath("rocksdb_sst_test.sst");
+ createSST(opts, file_path);
+
+ char* usage[3];
+ PopulateCommandArgs(file_path, "--command=verify_checksum", usage);
+
+ ROCKSDB_NAMESPACE::SSTDumpTool tool;
+ ASSERT_TRUE(!tool.Run(3, usage, opts));
+
+ cleanup(opts, file_path);
+ for (int i = 0; i < 3; i++) {
+ delete[] usage[i];
+ }
+}
+
+TEST_F(SSTDumpToolTest, ReadaheadSize) {
+ Options opts;
+ opts.env = env();
+ std::string file_path = MakeFilePath("rocksdb_sst_test.sst");
+ createSST(opts, file_path);
+
+ char* usage[4];
+ PopulateCommandArgs(file_path, "--command=verify", usage);
+ snprintf(usage[3], kOptLength, "--readahead_size=4000000");
+
+ int num_reads = 0;
+ SyncPoint::GetInstance()->SetCallBack("RandomAccessFileReader::Read",
+ [&](void*) { num_reads++; });
+ SyncPoint::GetInstance()->EnableProcessing();
+
+ SSTDumpTool tool;
+ ASSERT_TRUE(!tool.Run(4, usage, opts));
+
+ // The file is approximately 10MB. Readahead is 4MB.
+ // We usually need 3 reads + one metadata read.
+ // One extra read is needed before opening the file for metadata.
+ ASSERT_EQ(5, num_reads);
+
+ SyncPoint::GetInstance()->ClearAllCallBacks();
+ SyncPoint::GetInstance()->DisableProcessing();
+
+ cleanup(opts, file_path);
+ for (int i = 0; i < 4; i++) {
+ delete[] usage[i];
+ }
+}
+
+TEST_F(SSTDumpToolTest, NoSstFile) {
+ Options opts;
+ opts.env = env();
+ std::string file_path = MakeFilePath("no_such_file.sst");
+ char* usage[3];
+ PopulateCommandArgs(file_path, "", usage);
+ ROCKSDB_NAMESPACE::SSTDumpTool tool;
+ for (const auto& command :
+ {"--command=check", "--command=dump", "--command=raw",
+ "--command=verify", "--command=recompress", "--command=verify_checksum",
+ "--show_properties"}) {
+ snprintf(usage[1], kOptLength, "%s", command);
+ ASSERT_TRUE(tool.Run(3, usage, opts));
+ }
+ for (int i = 0; i < 3; i++) {
+ delete[] usage[i];
+ }
+}
+
+TEST_F(SSTDumpToolTest, ValidSSTPath) {
+ Options opts;
+ opts.env = env();
+ char* usage[3];
+ PopulateCommandArgs("", "", usage);
+ SSTDumpTool tool;
+ std::string file_not_exists = MakeFilePath("file_not_exists.sst");
+ std::string sst_file = MakeFilePath("rocksdb_sst_test.sst");
+ createSST(opts, sst_file);
+ std::string text_file = MakeFilePath("text_file");
+ ASSERT_OK(WriteStringToFile(opts.env, "Hello World!", text_file));
+ std::string fake_sst = MakeFilePath("fake_sst.sst");
+ ASSERT_OK(WriteStringToFile(opts.env, "Not an SST file!", fake_sst));
+
+ for (const auto& command_arg : {"--command=verify", "--command=identify"}) {
+ snprintf(usage[1], kOptLength, "%s", command_arg);
+
+ snprintf(usage[2], kOptLength, "--file=%s", file_not_exists.c_str());
+ ASSERT_TRUE(tool.Run(3, usage, opts));
+
+ snprintf(usage[2], kOptLength, "--file=%s", sst_file.c_str());
+ ASSERT_TRUE(!tool.Run(3, usage, opts));
+
+ snprintf(usage[2], kOptLength, "--file=%s", text_file.c_str());
+ ASSERT_TRUE(tool.Run(3, usage, opts));
+
+ snprintf(usage[2], kOptLength, "--file=%s", fake_sst.c_str());
+ ASSERT_TRUE(tool.Run(3, usage, opts));
+ }
+ ASSERT_OK(opts.env->DeleteFile(sst_file));
+ ASSERT_OK(opts.env->DeleteFile(text_file));
+ ASSERT_OK(opts.env->DeleteFile(fake_sst));
+
+ for (int i = 0; i < 3; i++) {
+ delete[] usage[i];
+ }
+}
+
+TEST_F(SSTDumpToolTest, RawOutput) {
+ Options opts;
+ opts.env = env();
+ std::string file_path = MakeFilePath("rocksdb_sst_test.sst");
+ createSST(opts, file_path);
+
+ char* usage[3];
+ PopulateCommandArgs(file_path, "--command=raw", usage);
+
+ ROCKSDB_NAMESPACE::SSTDumpTool tool;
+ ASSERT_TRUE(!tool.Run(3, usage, opts));
+
+ const std::string raw_path = MakeFilePath("rocksdb_sst_test_dump.txt");
+ std::ifstream raw_file(raw_path);
+
+ std::string tp;
+ bool is_data_block = false;
+ int key_count = 0;
+ while (getline(raw_file, tp)) {
+ if (tp.find("Data Block #") != std::string::npos) {
+ is_data_block = true;
+ }
+
+ if (is_data_block && tp.find("HEX") != std::string::npos) {
+ key_count++;
+ }
+ }
+
+ ASSERT_EQ(kNumKey, key_count);
+
+ raw_file.close();
+
+ cleanup(opts, file_path);
+ for (int i = 0; i < 3; i++) {
+ delete[] usage[i];
+ }
+}
+
+} // namespace ROCKSDB_NAMESPACE
+
+int main(int argc, char** argv) {
+ ROCKSDB_NAMESPACE::port::InstallStackTraceHandler();
+ ::testing::InitGoogleTest(&argc, argv);
+ RegisterCustomObjects(argc, argv);
+ return RUN_ALL_TESTS();
+}
+
+#else
+#include <stdio.h>
+
+int main(int /*argc*/, char** /*argv*/) {
+ fprintf(stderr, "SKIPPED as SSTDumpTool is not supported in ROCKSDB_LITE\n");
+ return 0;
+}
+
+#endif // !ROCKSDB_LITE return RUN_ALL_TESTS();
diff --git a/src/rocksdb/tools/sst_dump_tool.cc b/src/rocksdb/tools/sst_dump_tool.cc
new file mode 100644
index 000000000..0a2c28280
--- /dev/null
+++ b/src/rocksdb/tools/sst_dump_tool.cc
@@ -0,0 +1,584 @@
+// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+// This source code is licensed under both the GPLv2 (found in the
+// COPYING file in the root directory) and Apache 2.0 License
+// (found in the LICENSE.Apache file in the root directory).
+//
+#ifndef ROCKSDB_LITE
+
+#include "rocksdb/sst_dump_tool.h"
+
+#include <cinttypes>
+#include <iostream>
+
+#include "options/options_helper.h"
+#include "port/port.h"
+#include "rocksdb/convenience.h"
+#include "rocksdb/utilities/ldb_cmd.h"
+#include "table/sst_file_dumper.h"
+
+namespace ROCKSDB_NAMESPACE {
+
+static const std::vector<std::pair<CompressionType, const char*>>
+ kCompressions = {
+ {CompressionType::kNoCompression, "kNoCompression"},
+ {CompressionType::kSnappyCompression, "kSnappyCompression"},
+ {CompressionType::kZlibCompression, "kZlibCompression"},
+ {CompressionType::kBZip2Compression, "kBZip2Compression"},
+ {CompressionType::kLZ4Compression, "kLZ4Compression"},
+ {CompressionType::kLZ4HCCompression, "kLZ4HCCompression"},
+ {CompressionType::kXpressCompression, "kXpressCompression"},
+ {CompressionType::kZSTD, "kZSTD"}};
+
+namespace {
+
+void print_help(bool to_stderr) {
+ std::string supported_compressions;
+ for (CompressionType ct : GetSupportedCompressions()) {
+ if (!supported_compressions.empty()) {
+ supported_compressions += ", ";
+ }
+ std::string str;
+ Status s = GetStringFromCompressionType(&str, ct);
+ assert(s.ok());
+ supported_compressions += str;
+ }
+ fprintf(
+ to_stderr ? stderr : stdout,
+ R"(sst_dump --file=<data_dir_OR_sst_file> [--command=check|scan|raw|recompress|identify]
+ --file=<data_dir_OR_sst_file>
+ Path to SST file or directory containing SST files
+
+ --env_uri=<uri of underlying Env>
+ URI of underlying Env, mutually exclusive with fs_uri
+
+ --fs_uri=<uri of underlying FileSystem>
+ URI of underlying FileSystem, mutually exclusive with env_uri
+
+ --command=check|scan|raw|verify|identify
+ check: Iterate over entries in files but don't print anything except if an error is encountered (default command)
+ scan: Iterate over entries in files and print them to screen
+ raw: Dump all the table contents to <file_name>_dump.txt
+ verify: Iterate all the blocks in files verifying checksum to detect possible corruption but don't print anything except if a corruption is encountered
+ recompress: reports the SST file size if recompressed with different
+ compression types
+ identify: Reports a file is a valid SST file or lists all valid SST files under a directory
+
+ --output_hex
+ Can be combined with scan command to print the keys and values in Hex
+
+ --decode_blob_index
+ Decode blob indexes and print them in a human-readable format during scans.
+
+ --from=<user_key>
+ Key to start reading from when executing check|scan
+
+ --to=<user_key>
+ Key to stop reading at when executing check|scan
+
+ --prefix=<user_key>
+ Returns all keys with this prefix when executing check|scan
+ Cannot be used in conjunction with --from
+
+ --read_num=<num>
+ Maximum number of entries to read when executing check|scan
+
+ --verify_checksum
+ Verify file checksum when executing check|scan
+
+ --input_key_hex
+ Can be combined with --from and --to to indicate that these values are encoded in Hex
+
+ --show_properties
+ Print table properties after iterating over the file when executing
+ check|scan|raw|identify
+
+ --set_block_size=<block_size>
+ Can be combined with --command=recompress to set the block size that will
+ be used when trying different compression algorithms
+
+ --compression_types=<comma-separated list of CompressionType members, e.g.,
+ kSnappyCompression>
+ Can be combined with --command=recompress to run recompression for this
+ list of compression types
+ Supported compression types: %s
+
+ --parse_internal_key=<0xKEY>
+ Convenience option to parse an internal key on the command line. Dumps the
+ internal key in hex format {'key' @ SN: type}
+
+ --compression_level_from=<compression_level>
+ Compression level to start compressing when executing recompress. One compression type
+ and compression_level_to must also be specified
+
+ --compression_level_to=<compression_level>
+ Compression level to stop compressing when executing recompress. One compression type
+ and compression_level_from must also be specified
+
+ --compression_max_dict_bytes=<uint32_t>
+ Maximum size of dictionary used to prime the compression library
+
+ --compression_zstd_max_train_bytes=<uint32_t>
+ Maximum size of training data passed to zstd's dictionary trainer
+
+ --compression_max_dict_buffer_bytes=<int64_t>
+ Limit on buffer size from which we collect samples for dictionary generation.
+
+ --compression_use_zstd_finalize_dict
+ Use zstd's finalizeDictionary() API instead of zstd's dictionary trainer to generate dictionary.
+)",
+ supported_compressions.c_str());
+}
+
+// arg_name would include all prefix, e.g. "--my_arg="
+// arg_val is the parses value.
+// True if there is a match. False otherwise.
+// Woud exit after printing errmsg if cannot be parsed.
+bool ParseIntArg(const char* arg, const std::string arg_name,
+ const std::string err_msg, int64_t* arg_val) {
+ if (strncmp(arg, arg_name.c_str(), arg_name.size()) == 0) {
+ std::string input_str = arg + arg_name.size();
+ std::istringstream iss(input_str);
+ iss >> *arg_val;
+ if (iss.fail()) {
+ fprintf(stderr, "%s\n", err_msg.c_str());
+ exit(1);
+ }
+ return true;
+ }
+ return false;
+}
+} // namespace
+
+int SSTDumpTool::Run(int argc, char const* const* argv, Options options) {
+ std::string env_uri, fs_uri;
+ const char* dir_or_file = nullptr;
+ uint64_t read_num = std::numeric_limits<uint64_t>::max();
+ std::string command;
+
+ char junk;
+ uint64_t n;
+ bool verify_checksum = false;
+ bool output_hex = false;
+ bool decode_blob_index = false;
+ bool input_key_hex = false;
+ bool has_from = false;
+ bool has_to = false;
+ bool use_from_as_prefix = false;
+ bool show_properties = false;
+ bool show_summary = false;
+ bool set_block_size = false;
+ bool has_compression_level_from = false;
+ bool has_compression_level_to = false;
+ bool has_specified_compression_types = false;
+ std::string from_key;
+ std::string to_key;
+ std::string block_size_str;
+ std::string compression_level_from_str;
+ std::string compression_level_to_str;
+ size_t block_size = 0;
+ size_t readahead_size = 2 * 1024 * 1024;
+ std::vector<std::pair<CompressionType, const char*>> compression_types;
+ uint64_t total_num_files = 0;
+ uint64_t total_num_data_blocks = 0;
+ uint64_t total_data_block_size = 0;
+ uint64_t total_index_block_size = 0;
+ uint64_t total_filter_block_size = 0;
+ int32_t compress_level_from = CompressionOptions::kDefaultCompressionLevel;
+ int32_t compress_level_to = CompressionOptions::kDefaultCompressionLevel;
+ uint32_t compression_max_dict_bytes =
+ ROCKSDB_NAMESPACE::CompressionOptions().max_dict_bytes;
+ uint32_t compression_zstd_max_train_bytes =
+ ROCKSDB_NAMESPACE::CompressionOptions().zstd_max_train_bytes;
+ uint64_t compression_max_dict_buffer_bytes =
+ ROCKSDB_NAMESPACE::CompressionOptions().max_dict_buffer_bytes;
+ bool compression_use_zstd_finalize_dict =
+ !ROCKSDB_NAMESPACE::CompressionOptions().use_zstd_dict_trainer;
+
+ int64_t tmp_val;
+
+ for (int i = 1; i < argc; i++) {
+ if (strncmp(argv[i], "--env_uri=", 10) == 0) {
+ env_uri = argv[i] + 10;
+ } else if (strncmp(argv[i], "--fs_uri=", 9) == 0) {
+ fs_uri = argv[i] + 9;
+ } else if (strncmp(argv[i], "--file=", 7) == 0) {
+ dir_or_file = argv[i] + 7;
+ } else if (strcmp(argv[i], "--output_hex") == 0) {
+ output_hex = true;
+ } else if (strcmp(argv[i], "--decode_blob_index") == 0) {
+ decode_blob_index = true;
+ } else if (strcmp(argv[i], "--input_key_hex") == 0) {
+ input_key_hex = true;
+ } else if (sscanf(argv[i], "--read_num=%lu%c", (unsigned long*)&n, &junk) ==
+ 1) {
+ read_num = n;
+ } else if (strcmp(argv[i], "--verify_checksum") == 0) {
+ verify_checksum = true;
+ } else if (strncmp(argv[i], "--command=", 10) == 0) {
+ command = argv[i] + 10;
+ } else if (strncmp(argv[i], "--from=", 7) == 0) {
+ from_key = argv[i] + 7;
+ has_from = true;
+ } else if (strncmp(argv[i], "--to=", 5) == 0) {
+ to_key = argv[i] + 5;
+ has_to = true;
+ } else if (strncmp(argv[i], "--prefix=", 9) == 0) {
+ from_key = argv[i] + 9;
+ use_from_as_prefix = true;
+ } else if (strcmp(argv[i], "--show_properties") == 0) {
+ show_properties = true;
+ } else if (strcmp(argv[i], "--show_summary") == 0) {
+ show_summary = true;
+ } else if (ParseIntArg(argv[i], "--set_block_size=",
+ "block size must be numeric", &tmp_val)) {
+ set_block_size = true;
+ block_size = static_cast<size_t>(tmp_val);
+ } else if (ParseIntArg(argv[i], "--readahead_size=",
+ "readahead_size must be numeric", &tmp_val)) {
+ readahead_size = static_cast<size_t>(tmp_val);
+ } else if (strncmp(argv[i], "--compression_types=", 20) == 0) {
+ std::string compression_types_csv = argv[i] + 20;
+ std::istringstream iss(compression_types_csv);
+ std::string compression_type;
+ has_specified_compression_types = true;
+ while (std::getline(iss, compression_type, ',')) {
+ auto iter = std::find_if(
+ kCompressions.begin(), kCompressions.end(),
+ [&compression_type](std::pair<CompressionType, const char*> curr) {
+ return curr.second == compression_type;
+ });
+ if (iter == kCompressions.end()) {
+ fprintf(stderr, "%s is not a valid CompressionType\n",
+ compression_type.c_str());
+ exit(1);
+ }
+ compression_types.emplace_back(*iter);
+ }
+ } else if (strncmp(argv[i], "--parse_internal_key=", 21) == 0) {
+ std::string in_key(argv[i] + 21);
+ try {
+ in_key = ROCKSDB_NAMESPACE::LDBCommand::HexToString(in_key);
+ } catch (...) {
+ std::cerr << "ERROR: Invalid key input '" << in_key
+ << "' Use 0x{hex representation of internal rocksdb key}"
+ << std::endl;
+ return -1;
+ }
+ Slice sl_key = ROCKSDB_NAMESPACE::Slice(in_key);
+ ParsedInternalKey ikey;
+ int retc = 0;
+ Status pik_status =
+ ParseInternalKey(sl_key, &ikey, true /* log_err_key */);
+ if (!pik_status.ok()) {
+ std::cerr << pik_status.getState() << "\n";
+ retc = -1;
+ }
+ fprintf(stdout, "key=%s\n", ikey.DebugString(true, true).c_str());
+ return retc;
+ } else if (ParseIntArg(argv[i], "--compression_level_from=",
+ "compression_level_from must be numeric",
+ &tmp_val)) {
+ has_compression_level_from = true;
+ compress_level_from = static_cast<int>(tmp_val);
+ } else if (ParseIntArg(argv[i], "--compression_level_to=",
+ "compression_level_to must be numeric", &tmp_val)) {
+ has_compression_level_to = true;
+ compress_level_to = static_cast<int>(tmp_val);
+ } else if (ParseIntArg(argv[i], "--compression_max_dict_bytes=",
+ "compression_max_dict_bytes must be numeric",
+ &tmp_val)) {
+ if (tmp_val < 0 || tmp_val > std::numeric_limits<uint32_t>::max()) {
+ fprintf(stderr, "compression_max_dict_bytes must be a uint32_t: '%s'\n",
+ argv[i]);
+ print_help(/*to_stderr*/ true);
+ return 1;
+ }
+ compression_max_dict_bytes = static_cast<uint32_t>(tmp_val);
+ } else if (ParseIntArg(argv[i], "--compression_zstd_max_train_bytes=",
+ "compression_zstd_max_train_bytes must be numeric",
+ &tmp_val)) {
+ if (tmp_val < 0 || tmp_val > std::numeric_limits<uint32_t>::max()) {
+ fprintf(stderr,
+ "compression_zstd_max_train_bytes must be a uint32_t: '%s'\n",
+ argv[i]);
+ print_help(/*to_stderr*/ true);
+ return 1;
+ }
+ compression_zstd_max_train_bytes = static_cast<uint32_t>(tmp_val);
+ } else if (ParseIntArg(argv[i], "--compression_max_dict_buffer_bytes=",
+ "compression_max_dict_buffer_bytes must be numeric",
+ &tmp_val)) {
+ if (tmp_val < 0) {
+ fprintf(stderr,
+ "compression_max_dict_buffer_bytes must be positive: '%s'\n",
+ argv[i]);
+ print_help(/*to_stderr*/ true);
+ return 1;
+ }
+ compression_max_dict_buffer_bytes = static_cast<uint64_t>(tmp_val);
+ } else if (strcmp(argv[i], "--compression_use_zstd_finalize_dict") == 0) {
+ compression_use_zstd_finalize_dict = true;
+ } else if (strcmp(argv[i], "--help") == 0) {
+ print_help(/*to_stderr*/ false);
+ return 0;
+ } else if (strcmp(argv[i], "--version") == 0) {
+ printf("%s\n", GetRocksBuildInfoAsString("sst_dump").c_str());
+ return 0;
+ } else {
+ fprintf(stderr, "Unrecognized argument '%s'\n\n", argv[i]);
+ print_help(/*to_stderr*/ true);
+ return 1;
+ }
+ }
+
+ if (has_compression_level_from && has_compression_level_to) {
+ if (!has_specified_compression_types || compression_types.size() != 1) {
+ fprintf(stderr, "Specify one compression type.\n\n");
+ exit(1);
+ }
+ } else if (has_compression_level_from || has_compression_level_to) {
+ fprintf(stderr,
+ "Specify both --compression_level_from and "
+ "--compression_level_to.\n\n");
+ exit(1);
+ }
+
+ if (use_from_as_prefix && has_from) {
+ fprintf(stderr, "Cannot specify --prefix and --from\n\n");
+ exit(1);
+ }
+
+ if (input_key_hex) {
+ if (has_from || use_from_as_prefix) {
+ from_key = ROCKSDB_NAMESPACE::LDBCommand::HexToString(from_key);
+ }
+ if (has_to) {
+ to_key = ROCKSDB_NAMESPACE::LDBCommand::HexToString(to_key);
+ }
+ }
+
+ if (dir_or_file == nullptr) {
+ fprintf(stderr, "file or directory must be specified.\n\n");
+ print_help(/*to_stderr*/ true);
+ exit(1);
+ }
+
+ std::shared_ptr<ROCKSDB_NAMESPACE::Env> env_guard;
+
+ // If caller of SSTDumpTool::Run(...) does not specify a different env other
+ // than Env::Default(), then try to load custom env based on env_uri/fs_uri.
+ // Otherwise, the caller is responsible for creating custom env.
+ {
+ ConfigOptions config_options;
+ config_options.env = options.env;
+ Status s = Env::CreateFromUri(config_options, env_uri, fs_uri, &options.env,
+ &env_guard);
+ if (!s.ok()) {
+ fprintf(stderr, "CreateEnvFromUri: %s\n", s.ToString().c_str());
+ exit(1);
+ } else {
+ fprintf(stdout, "options.env is %p\n", options.env);
+ }
+ }
+
+ std::vector<std::string> filenames;
+ ROCKSDB_NAMESPACE::Env* env = options.env;
+ ROCKSDB_NAMESPACE::Status st = env->GetChildren(dir_or_file, &filenames);
+ bool dir = true;
+ if (!st.ok() || filenames.empty()) {
+ // dir_or_file does not exist or does not contain children
+ // Check its existence first
+ Status s = env->FileExists(dir_or_file);
+ // dir_or_file does not exist
+ if (!s.ok()) {
+ fprintf(stderr, "%s%s: No such file or directory\n", s.ToString().c_str(),
+ dir_or_file);
+ return 1;
+ }
+ // dir_or_file exists and is treated as a "file"
+ // since it has no children
+ // This is ok since later it will be checked
+ // that whether it is a valid sst or not
+ // (A directory "file" is not a valid sst)
+ filenames.clear();
+ filenames.push_back(dir_or_file);
+ dir = false;
+ }
+
+ uint64_t total_read = 0;
+ // List of RocksDB SST file without corruption
+ std::vector<std::string> valid_sst_files;
+ for (size_t i = 0; i < filenames.size(); i++) {
+ std::string filename = filenames.at(i);
+ if (filename.length() <= 4 ||
+ filename.rfind(".sst") != filename.length() - 4) {
+ // ignore
+ continue;
+ }
+
+ if (dir) {
+ filename = std::string(dir_or_file) + "/" + filename;
+ }
+
+ ROCKSDB_NAMESPACE::SstFileDumper dumper(
+ options, filename, Temperature::kUnknown, readahead_size,
+ verify_checksum, output_hex, decode_blob_index);
+ // Not a valid SST
+ if (!dumper.getStatus().ok()) {
+ fprintf(stderr, "%s: %s\n", filename.c_str(),
+ dumper.getStatus().ToString().c_str());
+ continue;
+ } else {
+ valid_sst_files.push_back(filename);
+ // Print out from and to key information once
+ // where there is at least one valid SST
+ if (valid_sst_files.size() == 1) {
+ // from_key and to_key are only used for "check", "scan", or ""
+ if (command == "check" || command == "scan" || command == "") {
+ fprintf(stdout, "from [%s] to [%s]\n",
+ ROCKSDB_NAMESPACE::Slice(from_key).ToString(true).c_str(),
+ ROCKSDB_NAMESPACE::Slice(to_key).ToString(true).c_str());
+ }
+ }
+ }
+
+ if (command == "recompress") {
+ st = dumper.ShowAllCompressionSizes(
+ set_block_size ? block_size : 16384,
+ compression_types.empty() ? kCompressions : compression_types,
+ compress_level_from, compress_level_to, compression_max_dict_bytes,
+ compression_zstd_max_train_bytes, compression_max_dict_buffer_bytes,
+ !compression_use_zstd_finalize_dict);
+ if (!st.ok()) {
+ fprintf(stderr, "Failed to recompress: %s\n", st.ToString().c_str());
+ exit(1);
+ }
+ return 0;
+ }
+
+ if (command == "raw") {
+ std::string out_filename = filename.substr(0, filename.length() - 4);
+ out_filename.append("_dump.txt");
+
+ st = dumper.DumpTable(out_filename);
+ if (!st.ok()) {
+ fprintf(stderr, "%s: %s\n", filename.c_str(), st.ToString().c_str());
+ exit(1);
+ } else {
+ fprintf(stdout, "raw dump written to file %s\n", &out_filename[0]);
+ }
+ continue;
+ }
+
+ // scan all files in give file path.
+ if (command == "" || command == "scan" || command == "check") {
+ st = dumper.ReadSequential(
+ command == "scan", read_num > 0 ? (read_num - total_read) : read_num,
+ has_from || use_from_as_prefix, from_key, has_to, to_key,
+ use_from_as_prefix);
+ if (!st.ok()) {
+ fprintf(stderr, "%s: %s\n", filename.c_str(), st.ToString().c_str());
+ }
+ total_read += dumper.GetReadNumber();
+ if (read_num > 0 && total_read > read_num) {
+ break;
+ }
+ }
+
+ if (command == "verify") {
+ st = dumper.VerifyChecksum();
+ if (!st.ok()) {
+ fprintf(stderr, "%s is corrupted: %s\n", filename.c_str(),
+ st.ToString().c_str());
+ } else {
+ fprintf(stdout, "The file is ok\n");
+ }
+ continue;
+ }
+
+ if (show_properties || show_summary) {
+ const ROCKSDB_NAMESPACE::TableProperties* table_properties;
+
+ std::shared_ptr<const ROCKSDB_NAMESPACE::TableProperties>
+ table_properties_from_reader;
+ st = dumper.ReadTableProperties(&table_properties_from_reader);
+ if (!st.ok()) {
+ fprintf(stderr, "%s: %s\n", filename.c_str(), st.ToString().c_str());
+ fprintf(stderr, "Try to use initial table properties\n");
+ table_properties = dumper.GetInitTableProperties();
+ } else {
+ table_properties = table_properties_from_reader.get();
+ }
+ if (table_properties != nullptr) {
+ if (show_properties) {
+ fprintf(stdout,
+ "Table Properties:\n"
+ "------------------------------\n"
+ " %s",
+ table_properties->ToString("\n ", ": ").c_str());
+ }
+ total_num_files += 1;
+ total_num_data_blocks += table_properties->num_data_blocks;
+ total_data_block_size += table_properties->data_size;
+ total_index_block_size += table_properties->index_size;
+ total_filter_block_size += table_properties->filter_size;
+ if (show_properties) {
+ fprintf(stdout,
+ "Raw user collected properties\n"
+ "------------------------------\n");
+ for (const auto& kv : table_properties->user_collected_properties) {
+ std::string prop_name = kv.first;
+ std::string prop_val = Slice(kv.second).ToString(true);
+ fprintf(stdout, " # %s: 0x%s\n", prop_name.c_str(),
+ prop_val.c_str());
+ }
+ }
+ } else {
+ fprintf(stderr, "Reader unexpectedly returned null properties\n");
+ }
+ }
+ }
+ if (show_summary) {
+ fprintf(stdout, "total number of files: %" PRIu64 "\n", total_num_files);
+ fprintf(stdout, "total number of data blocks: %" PRIu64 "\n",
+ total_num_data_blocks);
+ fprintf(stdout, "total data block size: %" PRIu64 "\n",
+ total_data_block_size);
+ fprintf(stdout, "total index block size: %" PRIu64 "\n",
+ total_index_block_size);
+ fprintf(stdout, "total filter block size: %" PRIu64 "\n",
+ total_filter_block_size);
+ }
+
+ if (valid_sst_files.empty()) {
+ // No valid SST files are found
+ // Exit with an error state
+ if (dir) {
+ fprintf(stdout, "------------------------------\n");
+ fprintf(stderr, "No valid SST files found in %s\n", dir_or_file);
+ } else {
+ fprintf(stderr, "%s is not a valid SST file\n", dir_or_file);
+ }
+ return 1;
+ } else {
+ if (command == "identify") {
+ if (dir) {
+ fprintf(stdout, "------------------------------\n");
+ fprintf(stdout, "List of valid SST files found in %s:\n", dir_or_file);
+ for (const auto& f : valid_sst_files) {
+ fprintf(stdout, "%s\n", f.c_str());
+ }
+ fprintf(stdout, "Number of valid SST files: %zu\n",
+ valid_sst_files.size());
+ } else {
+ fprintf(stdout, "%s is a valid SST file\n", dir_or_file);
+ }
+ }
+ // At least one valid SST
+ // exit with a success state
+ return 0;
+ }
+}
+} // namespace ROCKSDB_NAMESPACE
+
+#endif // ROCKSDB_LITE
diff --git a/src/rocksdb/tools/trace_analyzer.cc b/src/rocksdb/tools/trace_analyzer.cc
new file mode 100644
index 000000000..958078d1c
--- /dev/null
+++ b/src/rocksdb/tools/trace_analyzer.cc
@@ -0,0 +1,25 @@
+// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+// This source code is licensed under both the GPLv2 (found in the
+// COPYING file in the root directory) and Apache 2.0 License
+// (found in the LICENSE.Apache file in the root directory).
+//
+#ifndef ROCKSDB_LITE
+#ifndef GFLAGS
+#include <cstdio>
+int main() {
+ fprintf(stderr, "Please install gflags to run rocksdb tools\n");
+ return 1;
+}
+#else
+#include "tools/trace_analyzer_tool.h"
+int main(int argc, char** argv) {
+ return ROCKSDB_NAMESPACE::trace_analyzer_tool(argc, argv);
+}
+#endif
+#else
+#include <stdio.h>
+int main(int /*argc*/, char** /*argv*/) {
+ fprintf(stderr, "Not supported in lite mode.\n");
+ return 1;
+}
+#endif // ROCKSDB_LITE
diff --git a/src/rocksdb/tools/trace_analyzer_test.cc b/src/rocksdb/tools/trace_analyzer_test.cc
new file mode 100644
index 000000000..d7f9e4da8
--- /dev/null
+++ b/src/rocksdb/tools/trace_analyzer_test.cc
@@ -0,0 +1,890 @@
+// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+// This source code is licensed under both the GPLv2 (found in the
+// COPYING file in the root directory) and Apache 2.0 License
+// (found in the LICENSE.Apache file in the root directory).
+//
+// Copyright (c) 2012 The LevelDB Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file. See the AUTHORS file for names of contributors.
+
+#ifndef ROCKSDB_LITE
+#ifndef GFLAGS
+#include <cstdio>
+int main() {
+ fprintf(stderr, "Please install gflags to run trace_analyzer test\n");
+ return 0;
+}
+#else
+
+#include <chrono>
+#include <cstdio>
+#include <cstdlib>
+#include <sstream>
+#include <thread>
+
+#include "db/db_test_util.h"
+#include "file/line_file_reader.h"
+#include "rocksdb/db.h"
+#include "rocksdb/env.h"
+#include "rocksdb/status.h"
+#include "rocksdb/trace_reader_writer.h"
+#include "test_util/testharness.h"
+#include "test_util/testutil.h"
+#include "tools/trace_analyzer_tool.h"
+#include "trace_replay/trace_replay.h"
+
+namespace ROCKSDB_NAMESPACE {
+
+namespace {
+static const int kMaxArgCount = 100;
+static const size_t kArgBufferSize = 100000;
+} // namespace
+
+// Note that, the QPS part verification of the analyzing result is not robost
+// enough and causes the failure in some rare cases. Disable them temporally and
+// wait for future refactor.
+
+// The helper functions for the test
+class TraceAnalyzerTest : public testing::Test {
+ public:
+ TraceAnalyzerTest() : rnd_(0xFB) {
+ // test_path_ = test::TmpDir() + "trace_analyzer_test";
+ test_path_ = test::PerThreadDBPath("trace_analyzer_test");
+ env_ = ROCKSDB_NAMESPACE::Env::Default();
+ env_->CreateDir(test_path_).PermitUncheckedError();
+ dbname_ = test_path_ + "/db";
+ }
+
+ ~TraceAnalyzerTest() override {}
+
+ void GenerateTrace(std::string trace_path) {
+ Options options;
+ options.create_if_missing = true;
+ options.merge_operator = MergeOperators::CreatePutOperator();
+ Slice upper_bound("a");
+ Slice lower_bound("abce");
+ ReadOptions ro;
+ ro.iterate_upper_bound = &upper_bound;
+ ro.iterate_lower_bound = &lower_bound;
+ WriteOptions wo;
+ TraceOptions trace_opt;
+ DB* db_ = nullptr;
+ std::string value;
+ std::unique_ptr<TraceWriter> trace_writer;
+ Iterator* single_iter = nullptr;
+
+ ASSERT_OK(
+ NewFileTraceWriter(env_, env_options_, trace_path, &trace_writer));
+ ASSERT_OK(DB::Open(options, dbname_, &db_));
+ ASSERT_OK(db_->StartTrace(trace_opt, std::move(trace_writer)));
+
+ WriteBatch batch;
+ ASSERT_OK(batch.Put("a", "aaaaaaaaa"));
+ ASSERT_OK(batch.Merge("b", "aaaaaaaaaaaaaaaaaaaa"));
+ ASSERT_OK(batch.Delete("c"));
+ ASSERT_OK(batch.SingleDelete("d"));
+ ASSERT_OK(batch.DeleteRange("e", "f"));
+ ASSERT_OK(db_->Write(wo, &batch));
+ std::vector<Slice> keys;
+ keys.push_back("a");
+ keys.push_back("b");
+ keys.push_back("df");
+ keys.push_back("gege");
+ keys.push_back("hjhjhj");
+ std::vector<std::string> values;
+ std::vector<Status> ss = db_->MultiGet(ro, keys, &values);
+ ASSERT_GE(ss.size(), 0);
+ ASSERT_OK(ss[0]);
+ ASSERT_NOK(ss[2]);
+ std::vector<ColumnFamilyHandle*> cfs(2, db_->DefaultColumnFamily());
+ std::vector<PinnableSlice> values2(keys.size());
+ db_->MultiGet(ro, 2, cfs.data(), keys.data(), values2.data(), ss.data(),
+ false);
+ ASSERT_OK(ss[0]);
+ db_->MultiGet(ro, db_->DefaultColumnFamily(), 2, keys.data() + 3,
+ values2.data(), ss.data(), false);
+ ASSERT_OK(db_->Get(ro, "a", &value));
+
+ single_iter = db_->NewIterator(ro);
+ single_iter->Seek("a");
+ ASSERT_OK(single_iter->status());
+ single_iter->SeekForPrev("b");
+ ASSERT_OK(single_iter->status());
+ delete single_iter;
+ std::this_thread::sleep_for(std::chrono::seconds(1));
+
+ db_->Get(ro, "g", &value).PermitUncheckedError();
+
+ ASSERT_OK(db_->EndTrace());
+
+ ASSERT_OK(env_->FileExists(trace_path));
+
+ std::unique_ptr<WritableFile> whole_f;
+ std::string whole_path = test_path_ + "/0.txt";
+ ASSERT_OK(env_->NewWritableFile(whole_path, &whole_f, env_options_));
+ std::string whole_str = "0x61\n0x62\n0x63\n0x64\n0x65\n0x66\n";
+ ASSERT_OK(whole_f->Append(whole_str));
+ delete db_;
+ ASSERT_OK(DestroyDB(dbname_, options));
+ }
+
+ void RunTraceAnalyzer(const std::vector<std::string>& args) {
+ char arg_buffer[kArgBufferSize];
+ char* argv[kMaxArgCount];
+ int argc = 0;
+ int cursor = 0;
+
+ for (const auto& arg : args) {
+ ASSERT_LE(cursor + arg.size() + 1, kArgBufferSize);
+ ASSERT_LE(argc + 1, kMaxArgCount);
+ snprintf(arg_buffer + cursor, arg.size() + 1, "%s", arg.c_str());
+
+ argv[argc++] = arg_buffer + cursor;
+ cursor += static_cast<int>(arg.size()) + 1;
+ }
+
+ ASSERT_EQ(0, ROCKSDB_NAMESPACE::trace_analyzer_tool(argc, argv));
+ }
+
+ void CheckFileContent(const std::vector<std::string>& cnt,
+ std::string file_path, bool full_content) {
+ const auto& fs = env_->GetFileSystem();
+ FileOptions fopts(env_options_);
+
+ ASSERT_OK(fs->FileExists(file_path, fopts.io_options, nullptr));
+ std::unique_ptr<FSSequentialFile> file;
+ ASSERT_OK(fs->NewSequentialFile(file_path, fopts, &file, nullptr));
+
+ LineFileReader lf_reader(std::move(file), file_path,
+ 4096 /* filereadahead_size */);
+
+ std::vector<std::string> result;
+ std::string line;
+ while (
+ lf_reader.ReadLine(&line, Env::IO_TOTAL /* rate_limiter_priority */)) {
+ result.push_back(line);
+ }
+
+ ASSERT_OK(lf_reader.GetStatus());
+
+ size_t min_size = std::min(cnt.size(), result.size());
+ for (size_t i = 0; i < min_size; i++) {
+ if (full_content) {
+ ASSERT_EQ(result[i], cnt[i]);
+ } else {
+ ASSERT_EQ(result[i][0], cnt[i][0]);
+ }
+ }
+
+ return;
+ }
+
+ void AnalyzeTrace(std::vector<std::string>& paras_diff,
+ std::string output_path, std::string trace_path) {
+ std::vector<std::string> paras = {"./trace_analyzer",
+ "-convert_to_human_readable_trace",
+ "-output_key_stats",
+ "-output_access_count_stats",
+ "-output_prefix=test",
+ "-output_prefix_cut=1",
+ "-output_time_series",
+ "-output_value_distribution",
+ "-output_qps_stats",
+ "-no_key",
+ "-no_print"};
+ for (auto& para : paras_diff) {
+ paras.push_back(para);
+ }
+ Status s = env_->FileExists(trace_path);
+ if (!s.ok()) {
+ GenerateTrace(trace_path);
+ }
+ ASSERT_OK(env_->CreateDir(output_path));
+ RunTraceAnalyzer(paras);
+ }
+
+ ROCKSDB_NAMESPACE::Env* env_;
+ EnvOptions env_options_;
+ std::string test_path_;
+ std::string dbname_;
+ Random rnd_;
+};
+
+TEST_F(TraceAnalyzerTest, Get) {
+ std::string trace_path = test_path_ + "/trace";
+ std::string output_path = test_path_ + "/get";
+ std::string file_path;
+ std::vector<std::string> paras = {
+ "-analyze_get=true", "-analyze_put=false",
+ "-analyze_delete=false", "-analyze_single_delete=false",
+ "-analyze_range_delete=false", "-analyze_iterator=false",
+ "-analyze_multiget=false"};
+ paras.push_back("-output_dir=" + output_path);
+ paras.push_back("-trace_path=" + trace_path);
+ paras.push_back("-key_space_dir=" + test_path_);
+ AnalyzeTrace(paras, output_path, trace_path);
+
+ // check the key_stats file
+ std::vector<std::string> k_stats = {"0 10 0 1 1.000000", "0 10 1 1 1.000000"};
+ file_path = output_path + "/test-get-0-accessed_key_stats.txt";
+ CheckFileContent(k_stats, file_path, true);
+
+ // Check the access count distribution
+ std::vector<std::string> k_dist = {"access_count: 1 num: 2"};
+ file_path = output_path + "/test-get-0-accessed_key_count_distribution.txt";
+ CheckFileContent(k_dist, file_path, true);
+
+ // Check the trace sequence
+ std::vector<std::string> k_sequence = {"1", "5", "2", "3", "4", "8",
+ "8", "8", "8", "8", "8", "8",
+ "8", "8", "0", "6", "7", "0"};
+ file_path = output_path + "/test-human_readable_trace.txt";
+ CheckFileContent(k_sequence, file_path, false);
+
+ // Check the prefix
+ std::vector<std::string> k_prefix = {"0 0 0 0.000000 0.000000 0x30",
+ "1 1 1 1.000000 1.000000 0x61"};
+ file_path = output_path + "/test-get-0-accessed_key_prefix_cut.txt";
+ CheckFileContent(k_prefix, file_path, true);
+
+ // Check the time series
+ std::vector<std::string> k_series = {"0 1533000630 0", "0 1533000630 1"};
+ file_path = output_path + "/test-get-0-time_series.txt";
+ CheckFileContent(k_series, file_path, false);
+
+ // Check the accessed key in whole key space
+ std::vector<std::string> k_whole_access = {"0 1"};
+ file_path = output_path + "/test-get-0-whole_key_stats.txt";
+ CheckFileContent(k_whole_access, file_path, true);
+
+ // Check the whole key prefix cut
+ std::vector<std::string> k_whole_prefix = {"0 0x61", "1 0x62", "2 0x63",
+ "3 0x64", "4 0x65", "5 0x66"};
+ file_path = output_path + "/test-get-0-whole_key_prefix_cut.txt";
+ CheckFileContent(k_whole_prefix, file_path, true);
+
+ /*
+ // Check the overall qps
+ std::vector<std::string> all_qps = {"1 0 0 0 0 0 0 0 0 1"};
+ file_path = output_path + "/test-qps_stats.txt";
+ CheckFileContent(all_qps, file_path, true);
+
+ // Check the qps of get
+ std::vector<std::string> get_qps = {"1"};
+ file_path = output_path + "/test-get-0-qps_stats.txt";
+ CheckFileContent(get_qps, file_path, true);
+
+ // Check the top k qps prefix cut
+ std::vector<std::string> top_qps = {"At time: 0 with QPS: 1",
+ "The prefix: 0x61 Access count: 1"};
+ file_path = output_path + "/test-get-0-accessed_top_k_qps_prefix_cut.txt";
+ CheckFileContent(top_qps, file_path, true);
+ */
+}
+
+// Test analyzing of Put
+TEST_F(TraceAnalyzerTest, Put) {
+ std::string trace_path = test_path_ + "/trace";
+ std::string output_path = test_path_ + "/put";
+ std::string file_path;
+ std::vector<std::string> paras = {
+ "-analyze_get=false", "-analyze_put=true",
+ "-analyze_delete=false", "-analyze_single_delete=false",
+ "-analyze_range_delete=false", "-analyze_iterator=false",
+ "-analyze_multiget=false"};
+ paras.push_back("-output_dir=" + output_path);
+ paras.push_back("-trace_path=" + trace_path);
+ paras.push_back("-key_space_dir=" + test_path_);
+ AnalyzeTrace(paras, output_path, trace_path);
+
+ // check the key_stats file
+ std::vector<std::string> k_stats = {"0 9 0 1 1.000000"};
+ file_path = output_path + "/test-put-0-accessed_key_stats.txt";
+ CheckFileContent(k_stats, file_path, true);
+
+ // Check the access count distribution
+ std::vector<std::string> k_dist = {"access_count: 1 num: 1"};
+ file_path = output_path + "/test-put-0-accessed_key_count_distribution.txt";
+ CheckFileContent(k_dist, file_path, true);
+
+ // Check the trace sequence
+ std::vector<std::string> k_sequence = {"1", "5", "2", "3", "4", "8",
+ "8", "8", "8", "8", "8", "8",
+ "8", "8", "0", "6", "7", "0"};
+ file_path = output_path + "/test-human_readable_trace.txt";
+ CheckFileContent(k_sequence, file_path, false);
+
+ // Check the prefix
+ std::vector<std::string> k_prefix = {"0 0 0 0.000000 0.000000 0x30"};
+ file_path = output_path + "/test-put-0-accessed_key_prefix_cut.txt";
+ CheckFileContent(k_prefix, file_path, true);
+
+ // Check the time series
+ std::vector<std::string> k_series = {"1 1533056278 0"};
+ file_path = output_path + "/test-put-0-time_series.txt";
+ CheckFileContent(k_series, file_path, false);
+
+ // Check the accessed key in whole key space
+ std::vector<std::string> k_whole_access = {"0 1"};
+ file_path = output_path + "/test-put-0-whole_key_stats.txt";
+ CheckFileContent(k_whole_access, file_path, true);
+
+ // Check the whole key prefix cut
+ std::vector<std::string> k_whole_prefix = {"0 0x61", "1 0x62", "2 0x63",
+ "3 0x64", "4 0x65", "5 0x66"};
+ file_path = output_path + "/test-put-0-whole_key_prefix_cut.txt";
+ CheckFileContent(k_whole_prefix, file_path, true);
+
+ // Check the overall qps
+ std::vector<std::string> all_qps = {"0 1 0 0 0 0 0 0 0 1"};
+ file_path = output_path + "/test-qps_stats.txt";
+ CheckFileContent(all_qps, file_path, true);
+
+ /*
+ // Check the qps of Put
+ std::vector<std::string> get_qps = {"1"};
+ file_path = output_path + "/test-put-0-qps_stats.txt";
+ CheckFileContent(get_qps, file_path, true);
+
+ // Check the top k qps prefix cut
+ std::vector<std::string> top_qps = {"At time: 0 with QPS: 1",
+ "The prefix: 0x61 Access count: 1"};
+ file_path = output_path + "/test-put-0-accessed_top_k_qps_prefix_cut.txt";
+ CheckFileContent(top_qps, file_path, true);
+
+ // Check the value size distribution
+ std::vector<std::string> value_dist = {
+ "Number_of_value_size_between 0 and 16 is: 1"};
+ file_path = output_path + "/test-put-0-accessed_value_size_distribution.txt";
+ CheckFileContent(value_dist, file_path, true);
+ */
+}
+
+// Test analyzing of delete
+TEST_F(TraceAnalyzerTest, Delete) {
+ std::string trace_path = test_path_ + "/trace";
+ std::string output_path = test_path_ + "/delete";
+ std::string file_path;
+ std::vector<std::string> paras = {
+ "-analyze_get=false", "-analyze_put=false",
+ "-analyze_delete=true", "-analyze_single_delete=false",
+ "-analyze_range_delete=false", "-analyze_iterator=false",
+ "-analyze_multiget=false"};
+ paras.push_back("-output_dir=" + output_path);
+ paras.push_back("-trace_path=" + trace_path);
+ paras.push_back("-key_space_dir=" + test_path_);
+ AnalyzeTrace(paras, output_path, trace_path);
+
+ // check the key_stats file
+ std::vector<std::string> k_stats = {"0 10 0 1 1.000000"};
+ file_path = output_path + "/test-delete-0-accessed_key_stats.txt";
+ CheckFileContent(k_stats, file_path, true);
+
+ // Check the access count distribution
+ std::vector<std::string> k_dist = {"access_count: 1 num: 1"};
+ file_path =
+ output_path + "/test-delete-0-accessed_key_count_distribution.txt";
+ CheckFileContent(k_dist, file_path, true);
+
+ // Check the trace sequence
+ std::vector<std::string> k_sequence = {"1", "5", "2", "3", "4", "8",
+ "8", "8", "8", "8", "8", "8",
+ "8", "8", "0", "6", "7", "0"};
+ file_path = output_path + "/test-human_readable_trace.txt";
+ CheckFileContent(k_sequence, file_path, false);
+
+ // Check the prefix
+ std::vector<std::string> k_prefix = {"0 0 0 0.000000 0.000000 0x30"};
+ file_path = output_path + "/test-delete-0-accessed_key_prefix_cut.txt";
+ CheckFileContent(k_prefix, file_path, true);
+
+ // Check the time series
+ std::vector<std::string> k_series = {"2 1533000630 0"};
+ file_path = output_path + "/test-delete-0-time_series.txt";
+ CheckFileContent(k_series, file_path, false);
+
+ // Check the accessed key in whole key space
+ std::vector<std::string> k_whole_access = {"2 1"};
+ file_path = output_path + "/test-delete-0-whole_key_stats.txt";
+ CheckFileContent(k_whole_access, file_path, true);
+
+ // Check the whole key prefix cut
+ std::vector<std::string> k_whole_prefix = {"0 0x61", "1 0x62", "2 0x63",
+ "3 0x64", "4 0x65", "5 0x66"};
+ file_path = output_path + "/test-delete-0-whole_key_prefix_cut.txt";
+ CheckFileContent(k_whole_prefix, file_path, true);
+
+ /*
+ // Check the overall qps
+ std::vector<std::string> all_qps = {"0 0 1 0 0 0 0 0 0 1"};
+ file_path = output_path + "/test-qps_stats.txt";
+ CheckFileContent(all_qps, file_path, true);
+
+ // Check the qps of Delete
+ std::vector<std::string> get_qps = {"1"};
+ file_path = output_path + "/test-delete-0-qps_stats.txt";
+ CheckFileContent(get_qps, file_path, true);
+
+ // Check the top k qps prefix cut
+ std::vector<std::string> top_qps = {"At time: 0 with QPS: 1",
+ "The prefix: 0x63 Access count: 1"};
+ file_path = output_path + "/test-delete-0-accessed_top_k_qps_prefix_cut.txt";
+ CheckFileContent(top_qps, file_path, true);
+ */
+}
+
+// Test analyzing of Merge
+TEST_F(TraceAnalyzerTest, Merge) {
+ std::string trace_path = test_path_ + "/trace";
+ std::string output_path = test_path_ + "/merge";
+ std::string file_path;
+ std::vector<std::string> paras = {
+ "-analyze_get=false", "-analyze_put=false",
+ "-analyze_delete=false", "-analyze_merge=true",
+ "-analyze_single_delete=false", "-analyze_range_delete=false",
+ "-analyze_iterator=false", "-analyze_multiget=false"};
+ paras.push_back("-output_dir=" + output_path);
+ paras.push_back("-trace_path=" + trace_path);
+ paras.push_back("-key_space_dir=" + test_path_);
+ AnalyzeTrace(paras, output_path, trace_path);
+
+ // check the key_stats file
+ std::vector<std::string> k_stats = {"0 20 0 1 1.000000"};
+ file_path = output_path + "/test-merge-0-accessed_key_stats.txt";
+ CheckFileContent(k_stats, file_path, true);
+
+ // Check the access count distribution
+ std::vector<std::string> k_dist = {"access_count: 1 num: 1"};
+ file_path = output_path + "/test-merge-0-accessed_key_count_distribution.txt";
+ CheckFileContent(k_dist, file_path, true);
+
+ // Check the trace sequence
+ std::vector<std::string> k_sequence = {"1", "5", "2", "3", "4", "8",
+ "8", "8", "8", "8", "8", "8",
+ "8", "8", "0", "6", "7", "0"};
+ file_path = output_path + "/test-human_readable_trace.txt";
+ CheckFileContent(k_sequence, file_path, false);
+
+ // Check the prefix
+ std::vector<std::string> k_prefix = {"0 0 0 0.000000 0.000000 0x30"};
+ file_path = output_path + "/test-merge-0-accessed_key_prefix_cut.txt";
+ CheckFileContent(k_prefix, file_path, true);
+
+ // Check the time series
+ std::vector<std::string> k_series = {"5 1533000630 0"};
+ file_path = output_path + "/test-merge-0-time_series.txt";
+ CheckFileContent(k_series, file_path, false);
+
+ // Check the accessed key in whole key space
+ std::vector<std::string> k_whole_access = {"1 1"};
+ file_path = output_path + "/test-merge-0-whole_key_stats.txt";
+ CheckFileContent(k_whole_access, file_path, true);
+
+ // Check the whole key prefix cut
+ std::vector<std::string> k_whole_prefix = {"0 0x61", "1 0x62", "2 0x63",
+ "3 0x64", "4 0x65", "5 0x66"};
+ file_path = output_path + "/test-merge-0-whole_key_prefix_cut.txt";
+ CheckFileContent(k_whole_prefix, file_path, true);
+
+ /*
+ // Check the overall qps
+ std::vector<std::string> all_qps = {"0 0 0 0 0 1 0 0 0 1"};
+ file_path = output_path + "/test-qps_stats.txt";
+ CheckFileContent(all_qps, file_path, true);
+
+ // Check the qps of Merge
+ std::vector<std::string> get_qps = {"1"};
+ file_path = output_path + "/test-merge-0-qps_stats.txt";
+ CheckFileContent(get_qps, file_path, true);
+
+ // Check the top k qps prefix cut
+ std::vector<std::string> top_qps = {"At time: 0 with QPS: 1",
+ "The prefix: 0x62 Access count: 1"};
+ file_path = output_path + "/test-merge-0-accessed_top_k_qps_prefix_cut.txt";
+ CheckFileContent(top_qps, file_path, true);
+ */
+
+ // Check the value size distribution
+ std::vector<std::string> value_dist = {
+ "Number_of_value_size_between 0 and 24 is: 1"};
+ file_path =
+ output_path + "/test-merge-0-accessed_value_size_distribution.txt";
+ CheckFileContent(value_dist, file_path, true);
+}
+
+// Test analyzing of SingleDelete
+TEST_F(TraceAnalyzerTest, SingleDelete) {
+ std::string trace_path = test_path_ + "/trace";
+ std::string output_path = test_path_ + "/single_delete";
+ std::string file_path;
+ std::vector<std::string> paras = {
+ "-analyze_get=false", "-analyze_put=false",
+ "-analyze_delete=false", "-analyze_merge=false",
+ "-analyze_single_delete=true", "-analyze_range_delete=false",
+ "-analyze_iterator=false", "-analyze_multiget=false"};
+ paras.push_back("-output_dir=" + output_path);
+ paras.push_back("-trace_path=" + trace_path);
+ paras.push_back("-key_space_dir=" + test_path_);
+ AnalyzeTrace(paras, output_path, trace_path);
+
+ // check the key_stats file
+ std::vector<std::string> k_stats = {"0 10 0 1 1.000000"};
+ file_path = output_path + "/test-single_delete-0-accessed_key_stats.txt";
+ CheckFileContent(k_stats, file_path, true);
+
+ // Check the access count distribution
+ std::vector<std::string> k_dist = {"access_count: 1 num: 1"};
+ file_path =
+ output_path + "/test-single_delete-0-accessed_key_count_distribution.txt";
+ CheckFileContent(k_dist, file_path, true);
+
+ // Check the trace sequence
+ std::vector<std::string> k_sequence = {"1", "5", "2", "3", "4", "8",
+ "8", "8", "8", "8", "8", "8",
+ "8", "8", "0", "6", "7", "0"};
+ file_path = output_path + "/test-human_readable_trace.txt";
+ CheckFileContent(k_sequence, file_path, false);
+
+ // Check the prefix
+ std::vector<std::string> k_prefix = {"0 0 0 0.000000 0.000000 0x30"};
+ file_path = output_path + "/test-single_delete-0-accessed_key_prefix_cut.txt";
+ CheckFileContent(k_prefix, file_path, true);
+
+ // Check the time series
+ std::vector<std::string> k_series = {"3 1533000630 0"};
+ file_path = output_path + "/test-single_delete-0-time_series.txt";
+ CheckFileContent(k_series, file_path, false);
+
+ // Check the accessed key in whole key space
+ std::vector<std::string> k_whole_access = {"3 1"};
+ file_path = output_path + "/test-single_delete-0-whole_key_stats.txt";
+ CheckFileContent(k_whole_access, file_path, true);
+
+ // Check the whole key prefix cut
+ std::vector<std::string> k_whole_prefix = {"0 0x61", "1 0x62", "2 0x63",
+ "3 0x64", "4 0x65", "5 0x66"};
+ file_path = output_path + "/test-single_delete-0-whole_key_prefix_cut.txt";
+ CheckFileContent(k_whole_prefix, file_path, true);
+
+ /*
+ // Check the overall qps
+ std::vector<std::string> all_qps = {"0 0 0 1 0 0 0 0 0 1"};
+ file_path = output_path + "/test-qps_stats.txt";
+ CheckFileContent(all_qps, file_path, true);
+
+ // Check the qps of SingleDelete
+ std::vector<std::string> get_qps = {"1"};
+ file_path = output_path + "/test-single_delete-0-qps_stats.txt";
+ CheckFileContent(get_qps, file_path, true);
+
+ // Check the top k qps prefix cut
+ std::vector<std::string> top_qps = {"At time: 0 with QPS: 1",
+ "The prefix: 0x64 Access count: 1"};
+ file_path =
+ output_path + "/test-single_delete-0-accessed_top_k_qps_prefix_cut.txt";
+ CheckFileContent(top_qps, file_path, true);
+ */
+}
+
+// Test analyzing of delete
+TEST_F(TraceAnalyzerTest, DeleteRange) {
+ std::string trace_path = test_path_ + "/trace";
+ std::string output_path = test_path_ + "/range_delete";
+ std::string file_path;
+ std::vector<std::string> paras = {
+ "-analyze_get=false", "-analyze_put=false",
+ "-analyze_delete=false", "-analyze_merge=false",
+ "-analyze_single_delete=false", "-analyze_range_delete=true",
+ "-analyze_iterator=false", "-analyze_multiget=false"};
+ paras.push_back("-output_dir=" + output_path);
+ paras.push_back("-trace_path=" + trace_path);
+ paras.push_back("-key_space_dir=" + test_path_);
+ AnalyzeTrace(paras, output_path, trace_path);
+
+ // check the key_stats file
+ std::vector<std::string> k_stats = {"0 10 0 1 1.000000", "0 10 1 1 1.000000"};
+ file_path = output_path + "/test-range_delete-0-accessed_key_stats.txt";
+ CheckFileContent(k_stats, file_path, true);
+
+ // Check the access count distribution
+ std::vector<std::string> k_dist = {"access_count: 1 num: 2"};
+ file_path =
+ output_path + "/test-range_delete-0-accessed_key_count_distribution.txt";
+ CheckFileContent(k_dist, file_path, true);
+
+ // Check the trace sequence
+ std::vector<std::string> k_sequence = {"1", "5", "2", "3", "4", "8",
+ "8", "8", "8", "8", "8", "8",
+ "8", "8", "0", "6", "7", "0"};
+ file_path = output_path + "/test-human_readable_trace.txt";
+ CheckFileContent(k_sequence, file_path, false);
+
+ // Check the prefix
+ std::vector<std::string> k_prefix = {"0 0 0 0.000000 0.000000 0x30",
+ "1 1 1 1.000000 1.000000 0x65"};
+ file_path = output_path + "/test-range_delete-0-accessed_key_prefix_cut.txt";
+ CheckFileContent(k_prefix, file_path, true);
+
+ // Check the time series
+ std::vector<std::string> k_series = {"4 1533000630 0", "4 1533060100 1"};
+ file_path = output_path + "/test-range_delete-0-time_series.txt";
+ CheckFileContent(k_series, file_path, false);
+
+ // Check the accessed key in whole key space
+ std::vector<std::string> k_whole_access = {"4 1", "5 1"};
+ file_path = output_path + "/test-range_delete-0-whole_key_stats.txt";
+ CheckFileContent(k_whole_access, file_path, true);
+
+ // Check the whole key prefix cut
+ std::vector<std::string> k_whole_prefix = {"0 0x61", "1 0x62", "2 0x63",
+ "3 0x64", "4 0x65", "5 0x66"};
+ file_path = output_path + "/test-range_delete-0-whole_key_prefix_cut.txt";
+ CheckFileContent(k_whole_prefix, file_path, true);
+
+ /*
+ // Check the overall qps
+ std::vector<std::string> all_qps = {"0 0 0 0 2 0 0 0 0 2"};
+ file_path = output_path + "/test-qps_stats.txt";
+ CheckFileContent(all_qps, file_path, true);
+
+ // Check the qps of DeleteRange
+ std::vector<std::string> get_qps = {"2"};
+ file_path = output_path + "/test-range_delete-0-qps_stats.txt";
+ CheckFileContent(get_qps, file_path, true);
+
+ // Check the top k qps prefix cut
+ std::vector<std::string> top_qps = {"At time: 0 with QPS: 2",
+ "The prefix: 0x65 Access count: 1",
+ "The prefix: 0x66 Access count: 1"};
+ file_path =
+ output_path + "/test-range_delete-0-accessed_top_k_qps_prefix_cut.txt";
+ CheckFileContent(top_qps, file_path, true);
+ */
+}
+
+// Test analyzing of Iterator
+TEST_F(TraceAnalyzerTest, Iterator) {
+ std::string trace_path = test_path_ + "/trace";
+ std::string output_path = test_path_ + "/iterator";
+ std::string file_path;
+ std::vector<std::string> paras = {
+ "-analyze_get=false", "-analyze_put=false",
+ "-analyze_delete=false", "-analyze_merge=false",
+ "-analyze_single_delete=false", "-analyze_range_delete=false",
+ "-analyze_iterator=true", "-analyze_multiget=false"};
+ paras.push_back("-output_dir=" + output_path);
+ paras.push_back("-trace_path=" + trace_path);
+ paras.push_back("-key_space_dir=" + test_path_);
+ AnalyzeTrace(paras, output_path, trace_path);
+
+ // Check the output of Seek
+ // check the key_stats file
+ std::vector<std::string> k_stats = {"0 10 0 1 1.000000"};
+ file_path = output_path + "/test-iterator_Seek-0-accessed_key_stats.txt";
+ CheckFileContent(k_stats, file_path, true);
+
+ // Check the access count distribution
+ std::vector<std::string> k_dist = {"access_count: 1 num: 1"};
+ file_path =
+ output_path + "/test-iterator_Seek-0-accessed_key_count_distribution.txt";
+ CheckFileContent(k_dist, file_path, true);
+
+ // Check the trace sequence
+ std::vector<std::string> k_sequence = {"1", "5", "2", "3", "4", "8",
+ "8", "8", "8", "8", "8", "8",
+ "8", "8", "0", "6", "7", "0"};
+ file_path = output_path + "/test-human_readable_trace.txt";
+ CheckFileContent(k_sequence, file_path, false);
+
+ // Check the prefix
+ std::vector<std::string> k_prefix = {"0 0 0 0.000000 0.000000 0x30"};
+ file_path = output_path + "/test-iterator_Seek-0-accessed_key_prefix_cut.txt";
+ CheckFileContent(k_prefix, file_path, true);
+
+ // Check the time series
+ std::vector<std::string> k_series = {"6 1 0"};
+ file_path = output_path + "/test-iterator_Seek-0-time_series.txt";
+ CheckFileContent(k_series, file_path, false);
+
+ // Check the accessed key in whole key space
+ std::vector<std::string> k_whole_access = {"0 1"};
+ file_path = output_path + "/test-iterator_Seek-0-whole_key_stats.txt";
+ CheckFileContent(k_whole_access, file_path, true);
+
+ // Check the whole key prefix cut
+ std::vector<std::string> k_whole_prefix = {"0 0x61", "1 0x62", "2 0x63",
+ "3 0x64", "4 0x65", "5 0x66"};
+ file_path = output_path + "/test-iterator_Seek-0-whole_key_prefix_cut.txt";
+ CheckFileContent(k_whole_prefix, file_path, true);
+
+ /*
+ // Check the overall qps
+ std::vector<std::string> all_qps = {"0 0 0 0 0 0 1 1 0 2"};
+ file_path = output_path + "/test-qps_stats.txt";
+ CheckFileContent(all_qps, file_path, true);
+
+ // Check the qps of Iterator_Seek
+ std::vector<std::string> get_qps = {"1"};
+ file_path = output_path + "/test-iterator_Seek-0-qps_stats.txt";
+ CheckFileContent(get_qps, file_path, true);
+
+ // Check the top k qps prefix cut
+ std::vector<std::string> top_qps = {"At time: 0 with QPS: 1",
+ "The prefix: 0x61 Access count: 1"};
+ file_path =
+ output_path + "/test-iterator_Seek-0-accessed_top_k_qps_prefix_cut.txt";
+ CheckFileContent(top_qps, file_path, true);
+ */
+
+ // Check the output of SeekForPrev
+ // check the key_stats file
+ k_stats = {"0 10 0 1 1.000000"};
+ file_path =
+ output_path + "/test-iterator_SeekForPrev-0-accessed_key_stats.txt";
+ CheckFileContent(k_stats, file_path, true);
+
+ // Check the access count distribution
+ k_dist = {"access_count: 1 num: 1"};
+ file_path =
+ output_path +
+ "/test-iterator_SeekForPrev-0-accessed_key_count_distribution.txt";
+ CheckFileContent(k_dist, file_path, true);
+
+ // Check the prefix
+ k_prefix = {"0 0 0 0.000000 0.000000 0x30"};
+ file_path =
+ output_path + "/test-iterator_SeekForPrev-0-accessed_key_prefix_cut.txt";
+ CheckFileContent(k_prefix, file_path, true);
+
+ // Check the time series
+ k_series = {"7 0 0"};
+ file_path = output_path + "/test-iterator_SeekForPrev-0-time_series.txt";
+ CheckFileContent(k_series, file_path, false);
+
+ // Check the accessed key in whole key space
+ k_whole_access = {"1 1"};
+ file_path = output_path + "/test-iterator_SeekForPrev-0-whole_key_stats.txt";
+ CheckFileContent(k_whole_access, file_path, true);
+
+ // Check the whole key prefix cut
+ k_whole_prefix = {"0 0x61", "1 0x62", "2 0x63", "3 0x64", "4 0x65", "5 0x66"};
+ file_path =
+ output_path + "/test-iterator_SeekForPrev-0-whole_key_prefix_cut.txt";
+ CheckFileContent(k_whole_prefix, file_path, true);
+
+ /*
+ // Check the qps of Iterator_SeekForPrev
+ get_qps = {"1"};
+ file_path = output_path + "/test-iterator_SeekForPrev-0-qps_stats.txt";
+ CheckFileContent(get_qps, file_path, true);
+
+ // Check the top k qps prefix cut
+ top_qps = {"At time: 0 with QPS: 1", "The prefix: 0x62 Access count: 1"};
+ file_path = output_path +
+ "/test-iterator_SeekForPrev-0-accessed_top_k_qps_prefix_cut.txt";
+ CheckFileContent(top_qps, file_path, true);
+ */
+}
+
+// Test analyzing of multiget
+TEST_F(TraceAnalyzerTest, MultiGet) {
+ std::string trace_path = test_path_ + "/trace";
+ std::string output_path = test_path_ + "/multiget";
+ std::string file_path;
+ std::vector<std::string> paras = {
+ "-analyze_get=false", "-analyze_put=false",
+ "-analyze_delete=false", "-analyze_merge=false",
+ "-analyze_single_delete=false", "-analyze_range_delete=true",
+ "-analyze_iterator=false", "-analyze_multiget=true"};
+ paras.push_back("-output_dir=" + output_path);
+ paras.push_back("-trace_path=" + trace_path);
+ paras.push_back("-key_space_dir=" + test_path_);
+ AnalyzeTrace(paras, output_path, trace_path);
+
+ // check the key_stats file
+ std::vector<std::string> k_stats = {"0 10 0 2 1.000000", "0 10 1 2 1.000000",
+ "0 10 2 1 1.000000", "0 10 3 2 1.000000",
+ "0 10 4 2 1.000000"};
+ file_path = output_path + "/test-multiget-0-accessed_key_stats.txt";
+ CheckFileContent(k_stats, file_path, true);
+
+ // Check the access count distribution
+ std::vector<std::string> k_dist = {"access_count: 1 num: 1",
+ "access_count: 2 num: 4"};
+ file_path =
+ output_path + "/test-multiget-0-accessed_key_count_distribution.txt";
+ CheckFileContent(k_dist, file_path, true);
+
+ // Check the trace sequence
+ std::vector<std::string> k_sequence = {"1", "5", "2", "3", "4", "8",
+ "8", "8", "8", "8", "8", "8",
+ "8", "8", "0", "6", "7", "0"};
+ file_path = output_path + "/test-human_readable_trace.txt";
+ CheckFileContent(k_sequence, file_path, false);
+
+ // Check the prefix
+ std::vector<std::string> k_prefix = {
+ "0 0 0 0.000000 0.000000 0x30", "1 2 1 2.000000 1.000000 0x61",
+ "2 2 1 2.000000 1.000000 0x62", "3 1 1 1.000000 1.000000 0x64",
+ "4 2 1 2.000000 1.000000 0x67"};
+ file_path = output_path + "/test-multiget-0-accessed_key_prefix_cut.txt";
+ CheckFileContent(k_prefix, file_path, true);
+
+ // Check the time series
+ std::vector<std::string> k_series = {"8 0 0", "8 0 1", "8 0 2",
+ "8 0 3", "8 0 4", "8 0 0",
+ "8 0 1", "8 0 3", "8 0 4"};
+ file_path = output_path + "/test-multiget-0-time_series.txt";
+ CheckFileContent(k_series, file_path, false);
+
+ // Check the accessed key in whole key space
+ std::vector<std::string> k_whole_access = {"0 2", "1 2"};
+ file_path = output_path + "/test-multiget-0-whole_key_stats.txt";
+ CheckFileContent(k_whole_access, file_path, true);
+
+ // Check the whole key prefix cut
+ std::vector<std::string> k_whole_prefix = {"0 0x61", "1 0x62", "2 0x63",
+ "3 0x64", "4 0x65", "5 0x66"};
+ file_path = output_path + "/test-multiget-0-whole_key_prefix_cut.txt";
+ CheckFileContent(k_whole_prefix, file_path, true);
+
+ /*
+ // Check the overall qps. We have 3 MultiGet queries and it requested 9 keys
+ // in total
+ std::vector<std::string> all_qps = {"0 0 0 0 2 0 0 0 9 11"};
+ file_path = output_path + "/test-qps_stats.txt";
+ CheckFileContent(all_qps, file_path, true);
+
+ // Check the qps of DeleteRange
+ std::vector<std::string> get_qps = {"9"};
+ file_path = output_path + "/test-multiget-0-qps_stats.txt";
+ CheckFileContent(get_qps, file_path, true);
+
+ // Check the top k qps prefix cut
+ std::vector<std::string> top_qps = {
+ "At time: 0 with QPS: 9", "The prefix: 0x61 Access count: 2",
+ "The prefix: 0x62 Access count: 2", "The prefix: 0x64 Access count: 1",
+ "The prefix: 0x67 Access count: 2", "The prefix: 0x68 Access count: 2"};
+ file_path =
+ output_path + "/test-multiget-0-accessed_top_k_qps_prefix_cut.txt";
+ CheckFileContent(top_qps, file_path, true);
+ */
+}
+
+} // namespace ROCKSDB_NAMESPACE
+
+int main(int argc, char** argv) {
+ ROCKSDB_NAMESPACE::port::InstallStackTraceHandler();
+ ::testing::InitGoogleTest(&argc, argv);
+ return RUN_ALL_TESTS();
+}
+#endif // GFLAG
+#else
+#include <stdio.h>
+
+int main(int /*argc*/, char** /*argv*/) {
+ fprintf(stderr, "Trace_analyzer test is not supported in ROCKSDB_LITE\n");
+ return 0;
+}
+
+#endif // !ROCKSDB_LITE return RUN_ALL_TESTS();
diff --git a/src/rocksdb/tools/trace_analyzer_tool.cc b/src/rocksdb/tools/trace_analyzer_tool.cc
new file mode 100644
index 000000000..5a6d67864
--- /dev/null
+++ b/src/rocksdb/tools/trace_analyzer_tool.cc
@@ -0,0 +1,1925 @@
+// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+// This source code is licensed under both the GPLv2 (found in the
+// COPYING file in the root directory) and Apache 2.0 License
+// (found in the LICENSE.Apache file in the root directory).
+//
+
+#ifndef ROCKSDB_LITE
+
+#ifdef GFLAGS
+#ifdef NUMA
+#include <numa.h>
+#endif
+#ifndef OS_WIN
+#include <unistd.h>
+#endif
+#include <cinttypes>
+#include <cmath>
+#include <cstdio>
+#include <cstdlib>
+#include <memory>
+#include <sstream>
+#include <stdexcept>
+
+#include "db/db_impl/db_impl.h"
+#include "db/memtable.h"
+#include "db/write_batch_internal.h"
+#include "env/composite_env_wrapper.h"
+#include "file/line_file_reader.h"
+#include "file/writable_file_writer.h"
+#include "options/cf_options.h"
+#include "rocksdb/db.h"
+#include "rocksdb/env.h"
+#include "rocksdb/iterator.h"
+#include "rocksdb/slice.h"
+#include "rocksdb/slice_transform.h"
+#include "rocksdb/status.h"
+#include "rocksdb/table_properties.h"
+#include "rocksdb/utilities/ldb_cmd.h"
+#include "rocksdb/write_batch.h"
+#include "table/meta_blocks.h"
+#include "table/table_reader.h"
+#include "tools/trace_analyzer_tool.h"
+#include "trace_replay/trace_replay.h"
+#include "util/coding.h"
+#include "util/compression.h"
+#include "util/gflags_compat.h"
+#include "util/random.h"
+#include "util/string_util.h"
+
+using GFLAGS_NAMESPACE::ParseCommandLineFlags;
+
+DEFINE_string(trace_path, "", "The trace file path.");
+DEFINE_string(output_dir, "", "The directory to store the output files.");
+DEFINE_string(output_prefix, "trace",
+ "The prefix used for all the output files.");
+DEFINE_bool(output_key_stats, false,
+ "Output the key access count statistics to file\n"
+ "for accessed keys:\n"
+ "file name: <prefix>-<query_type>-<cf_id>-accessed_key_stats.txt\n"
+ "Format:[cf_id value_size access_keyid access_count]\n"
+ "for the whole key space keys:\n"
+ "File name: <prefix>-<query_type>-<cf_id>-whole_key_stats.txt\n"
+ "Format:[whole_key_space_keyid access_count]");
+DEFINE_bool(output_access_count_stats, false,
+ "Output the access count distribution statistics to file.\n"
+ "File name: <prefix>-<query_type>-<cf_id>-accessed_"
+ "key_count_distribution.txt \n"
+ "Format:[access_count number_of_access_count]");
+DEFINE_bool(output_time_series, false,
+ "Output the access time in second of each key, "
+ "such that we can have the time series data of the queries \n"
+ "File name: <prefix>-<query_type>-<cf_id>-time_series.txt\n"
+ "Format:[type_id time_in_sec access_keyid].");
+DEFINE_bool(try_process_corrupted_trace, false,
+ "In default, trace_analyzer will exit if the trace file is "
+ "corrupted due to the unexpected tracing cases. If this option "
+ "is enabled, trace_analyzer will stop reading the trace file, "
+ "and start analyzing the read-in data.");
+DEFINE_int32(output_prefix_cut, 0,
+ "The number of bytes as prefix to cut the keys.\n"
+ "If it is enabled, it will generate the following:\n"
+ "For accessed keys:\n"
+ "File name: <prefix>-<query_type>-<cf_id>-"
+ "accessed_key_prefix_cut.txt \n"
+ "Format:[acessed_keyid access_count_of_prefix "
+ "number_of_keys_in_prefix average_key_access "
+ "prefix_succ_ratio prefix]\n"
+ "For whole key space keys:\n"
+ "File name: <prefix>-<query_type>-<cf_id>"
+ "-whole_key_prefix_cut.txt\n"
+ "Format:[start_keyid_in_whole_keyspace prefix]\n"
+ "if 'output_qps_stats' and 'top_k' are enabled, it will output:\n"
+ "File name: <prefix>-<query_type>-<cf_id>"
+ "-accessed_top_k_qps_prefix_cut.txt\n"
+ "Format:[the_top_ith_qps_time QPS], [prefix qps_of_this_second].");
+DEFINE_bool(convert_to_human_readable_trace, false,
+ "Convert the binary trace file to a human readable txt file "
+ "for further processing. "
+ "This file will be extremely large "
+ "(similar size as the original binary trace file). "
+ "You can specify 'no_key' to reduce the size, if key is not "
+ "needed in the next step.\n"
+ "File name: <prefix>_human_readable_trace.txt\n"
+ "Format:[<key> type_id cf_id value_size time_in_micorsec].");
+DEFINE_bool(output_qps_stats, false,
+ "Output the query per second(qps) statistics \n"
+ "For the overall qps, it will contain all qps of each query type. "
+ "The time is started from the first trace record\n"
+ "File name: <prefix>_qps_stats.txt\n"
+ "Format: [qps_type_1 qps_type_2 ...... overall_qps]\n"
+ "For each cf and query, it will have its own qps output.\n"
+ "File name: <prefix>-<query_type>-<cf_id>_qps_stats.txt \n"
+ "Format:[query_count_in_this_second].");
+DEFINE_bool(no_print, false, "Do not print out any result");
+DEFINE_string(
+ print_correlation, "",
+ "intput format: [correlation pairs][.,.]\n"
+ "Output the query correlations between the pairs of query types "
+ "listed in the parameter, input should select the operations from:\n"
+ "get, put, delete, single_delete, rangle_delete, merge. No space "
+ "between the pairs separated by commar. Example: =[get,get]... "
+ "It will print out the number of pairs of 'A after B' and "
+ "the average time interval between the two query.");
+DEFINE_string(key_space_dir, "",
+ "<the directory stores full key space files> \n"
+ "The key space files should be: <column family id>.txt");
+DEFINE_bool(analyze_get, false, "Analyze the Get query.");
+DEFINE_bool(analyze_put, false, "Analyze the Put query.");
+DEFINE_bool(analyze_delete, false, "Analyze the Delete query.");
+DEFINE_bool(analyze_single_delete, false, "Analyze the SingleDelete query.");
+DEFINE_bool(analyze_range_delete, false, "Analyze the DeleteRange query.");
+DEFINE_bool(analyze_merge, false, "Analyze the Merge query.");
+DEFINE_bool(analyze_iterator, false,
+ " Analyze the iterate query like Seek() and SeekForPrev().");
+DEFINE_bool(analyze_multiget, false,
+ " Analyze the MultiGet query. NOTE: for"
+ " MultiGet, we analyze each KV-pair read in one MultiGet query. "
+ "Therefore, the total queries and QPS are calculated based on "
+ "the number of KV-pairs being accessed not the number of MultiGet."
+ "It can be improved in the future if needed");
+DEFINE_bool(no_key, false,
+ " Does not output the key to the result files to make smaller.");
+DEFINE_bool(print_overall_stats, true,
+ " Print the stats of the whole trace, "
+ "like total requests, keys, and etc.");
+DEFINE_bool(output_key_distribution, false, "Print the key size distribution.");
+DEFINE_bool(
+ output_value_distribution, false,
+ "Out put the value size distribution, only available for Put and Merge.\n"
+ "File name: <prefix>-<query_type>-<cf_id>"
+ "-accessed_value_size_distribution.txt\n"
+ "Format:[Number_of_value_size_between x and "
+ "x+value_interval is: <the count>]");
+DEFINE_int32(print_top_k_access, 1,
+ "<top K of the variables to be printed> "
+ "Print the top k accessed keys, top k accessed prefix "
+ "and etc.");
+DEFINE_int32(output_ignore_count, 0,
+ "<threshold>, ignores the access count <= this value, "
+ "it will shorter the output.");
+DEFINE_int32(value_interval, 8,
+ "To output the value distribution, we need to set the value "
+ "intervals and make the statistic of the value size distribution "
+ "in different intervals. The default is 8.");
+DEFINE_double(sample_ratio, 1.0,
+ "If the trace size is extremely huge or user want to sample "
+ "the trace when analyzing, sample ratio can be set (0, 1.0]");
+
+namespace ROCKSDB_NAMESPACE {
+
+const size_t kShadowValueSize = 10;
+
+std::map<std::string, int> taOptToIndex = {
+ {"get", 0}, {"put", 1},
+ {"delete", 2}, {"single_delete", 3},
+ {"range_delete", 4}, {"merge", 5},
+ {"iterator_Seek", 6}, {"iterator_SeekForPrev", 7},
+ {"multiget", 8}};
+
+std::map<int, std::string> taIndexToOpt = {
+ {0, "get"}, {1, "put"},
+ {2, "delete"}, {3, "single_delete"},
+ {4, "range_delete"}, {5, "merge"},
+ {6, "iterator_Seek"}, {7, "iterator_SeekForPrev"},
+ {8, "multiget"}};
+
+namespace {
+
+uint64_t MultiplyCheckOverflow(uint64_t op1, uint64_t op2) {
+ if (op1 == 0 || op2 == 0) {
+ return 0;
+ }
+ if (std::numeric_limits<uint64_t>::max() / op1 < op2) {
+ return op1;
+ }
+ return (op1 * op2);
+}
+
+} // namespace
+
+// The default constructor of AnalyzerOptions
+AnalyzerOptions::AnalyzerOptions()
+ : correlation_map(kTaTypeNum, std::vector<int>(kTaTypeNum, -1)) {}
+
+AnalyzerOptions::~AnalyzerOptions() {}
+
+void AnalyzerOptions::SparseCorrelationInput(const std::string& in_str) {
+ std::string cur = in_str;
+ if (cur.size() == 0) {
+ return;
+ }
+ while (!cur.empty()) {
+ if (cur.compare(0, 1, "[") != 0) {
+ fprintf(stderr, "Invalid correlation input: %s\n", in_str.c_str());
+ exit(1);
+ }
+ std::string opt1, opt2;
+ std::size_t split = cur.find_first_of(",");
+ if (split != std::string::npos) {
+ opt1 = cur.substr(1, split - 1);
+ } else {
+ fprintf(stderr, "Invalid correlation input: %s\n", in_str.c_str());
+ exit(1);
+ }
+ std::size_t end = cur.find_first_of("]");
+ if (end != std::string::npos) {
+ opt2 = cur.substr(split + 1, end - split - 1);
+ } else {
+ fprintf(stderr, "Invalid correlation input: %s\n", in_str.c_str());
+ exit(1);
+ }
+ cur = cur.substr(end + 1);
+
+ if (taOptToIndex.find(opt1) != taOptToIndex.end() &&
+ taOptToIndex.find(opt2) != taOptToIndex.end()) {
+ correlation_list.push_back(
+ std::make_pair(taOptToIndex[opt1], taOptToIndex[opt2]));
+ } else {
+ fprintf(stderr, "Invalid correlation input: %s\n", in_str.c_str());
+ exit(1);
+ }
+ }
+
+ int sequence = 0;
+ for (auto& it : correlation_list) {
+ correlation_map[it.first][it.second] = sequence;
+ sequence++;
+ }
+ return;
+}
+
+// The trace statistic struct constructor
+TraceStats::TraceStats() {
+ cf_id = 0;
+ cf_name = "0";
+ a_count = 0;
+ a_key_id = 0;
+ a_key_size_sqsum = 0;
+ a_key_size_sum = 0;
+ a_key_mid = 0;
+ a_value_size_sqsum = 0;
+ a_value_size_sum = 0;
+ a_value_mid = 0;
+ a_peak_qps = 0;
+ a_ave_qps = 0.0;
+}
+
+TraceStats::~TraceStats() {}
+
+// The trace analyzer constructor
+TraceAnalyzer::TraceAnalyzer(std::string& trace_path, std::string& output_path,
+ AnalyzerOptions _analyzer_opts)
+ : write_batch_ts_(0),
+ trace_name_(trace_path),
+ output_path_(output_path),
+ analyzer_opts_(_analyzer_opts) {
+ ROCKSDB_NAMESPACE::EnvOptions env_options;
+ env_ = ROCKSDB_NAMESPACE::Env::Default();
+ offset_ = 0;
+ total_requests_ = 0;
+ total_access_keys_ = 0;
+ total_gets_ = 0;
+ total_writes_ = 0;
+ total_seeks_ = 0;
+ total_seek_prevs_ = 0;
+ total_multigets_ = 0;
+ trace_create_time_ = 0;
+ begin_time_ = 0;
+ end_time_ = 0;
+ time_series_start_ = 0;
+ cur_time_sec_ = 0;
+ if (FLAGS_sample_ratio > 1.0 || FLAGS_sample_ratio <= 0) {
+ sample_max_ = 1;
+ } else {
+ sample_max_ = static_cast<uint32_t>(1.0 / FLAGS_sample_ratio);
+ }
+
+ ta_.resize(kTaTypeNum);
+ ta_[0].type_name = "get";
+ if (FLAGS_analyze_get) {
+ ta_[0].enabled = true;
+ } else {
+ ta_[0].enabled = false;
+ }
+ ta_[1].type_name = "put";
+ if (FLAGS_analyze_put) {
+ ta_[1].enabled = true;
+ } else {
+ ta_[1].enabled = false;
+ }
+ ta_[2].type_name = "delete";
+ if (FLAGS_analyze_delete) {
+ ta_[2].enabled = true;
+ } else {
+ ta_[2].enabled = false;
+ }
+ ta_[3].type_name = "single_delete";
+ if (FLAGS_analyze_single_delete) {
+ ta_[3].enabled = true;
+ } else {
+ ta_[3].enabled = false;
+ }
+ ta_[4].type_name = "range_delete";
+ if (FLAGS_analyze_range_delete) {
+ ta_[4].enabled = true;
+ } else {
+ ta_[4].enabled = false;
+ }
+ ta_[5].type_name = "merge";
+ if (FLAGS_analyze_merge) {
+ ta_[5].enabled = true;
+ } else {
+ ta_[5].enabled = false;
+ }
+ ta_[6].type_name = "iterator_Seek";
+ if (FLAGS_analyze_iterator) {
+ ta_[6].enabled = true;
+ } else {
+ ta_[6].enabled = false;
+ }
+ ta_[7].type_name = "iterator_SeekForPrev";
+ if (FLAGS_analyze_iterator) {
+ ta_[7].enabled = true;
+ } else {
+ ta_[7].enabled = false;
+ }
+ ta_[8].type_name = "multiget";
+ if (FLAGS_analyze_multiget) {
+ ta_[8].enabled = true;
+ } else {
+ ta_[8].enabled = false;
+ }
+ for (int i = 0; i < kTaTypeNum; i++) {
+ ta_[i].sample_count = 0;
+ }
+}
+
+TraceAnalyzer::~TraceAnalyzer() {}
+
+// Prepare the processing
+// Initiate the global trace reader and writer here
+Status TraceAnalyzer::PrepareProcessing() {
+ Status s;
+ // Prepare the trace reader
+ if (trace_reader_ == nullptr) {
+ s = NewFileTraceReader(env_, env_options_, trace_name_, &trace_reader_);
+ } else {
+ s = trace_reader_->Reset();
+ }
+ if (!s.ok()) {
+ return s;
+ }
+
+ // Prepare and open the trace sequence file writer if needed
+ if (FLAGS_convert_to_human_readable_trace) {
+ std::string trace_sequence_name;
+ trace_sequence_name =
+ output_path_ + "/" + FLAGS_output_prefix + "-human_readable_trace.txt";
+ s = env_->NewWritableFile(trace_sequence_name, &trace_sequence_f_,
+ env_options_);
+ if (!s.ok()) {
+ return s;
+ }
+ }
+
+ // prepare the general QPS file writer
+ if (FLAGS_output_qps_stats) {
+ std::string qps_stats_name;
+ qps_stats_name =
+ output_path_ + "/" + FLAGS_output_prefix + "-qps_stats.txt";
+ s = env_->NewWritableFile(qps_stats_name, &qps_f_, env_options_);
+ if (!s.ok()) {
+ return s;
+ }
+
+ qps_stats_name =
+ output_path_ + "/" + FLAGS_output_prefix + "-cf_qps_stats.txt";
+ s = env_->NewWritableFile(qps_stats_name, &cf_qps_f_, env_options_);
+ if (!s.ok()) {
+ return s;
+ }
+ }
+ return Status::OK();
+}
+
+Status TraceAnalyzer::ReadTraceHeader(Trace* header) {
+ assert(header != nullptr);
+ std::string encoded_trace;
+ // Read the trace head
+ Status s = trace_reader_->Read(&encoded_trace);
+ if (!s.ok()) {
+ return s;
+ }
+
+ s = TracerHelper::DecodeTrace(encoded_trace, header);
+
+ if (header->type != kTraceBegin) {
+ return Status::Corruption("Corrupted trace file. Incorrect header.");
+ }
+ if (header->payload.substr(0, kTraceMagic.length()) != kTraceMagic) {
+ return Status::Corruption("Corrupted trace file. Incorrect magic.");
+ }
+
+ return s;
+}
+
+Status TraceAnalyzer::ReadTraceFooter(Trace* footer) {
+ assert(footer != nullptr);
+ Status s = ReadTraceRecord(footer);
+ if (!s.ok()) {
+ return s;
+ }
+ if (footer->type != kTraceEnd) {
+ return Status::Corruption("Corrupted trace file. Incorrect footer.");
+ }
+ return s;
+}
+
+Status TraceAnalyzer::ReadTraceRecord(Trace* trace) {
+ assert(trace != nullptr);
+ std::string encoded_trace;
+ Status s = trace_reader_->Read(&encoded_trace);
+ if (!s.ok()) {
+ return s;
+ }
+ return TracerHelper::DecodeTrace(encoded_trace, trace);
+}
+
+// process the trace itself and redirect the trace content
+// to different operation type handler. With different race
+// format, this function can be changed
+Status TraceAnalyzer::StartProcessing() {
+ Status s;
+ Trace header;
+ s = ReadTraceHeader(&header);
+ if (!s.ok()) {
+ fprintf(stderr, "Cannot read the header\n");
+ return s;
+ }
+ // Set the default trace file version as version 0.2
+ int trace_file_version = 2;
+ s = TracerHelper::ParseTraceHeader(header, &trace_file_version, &db_version_);
+ if (!s.ok()) {
+ return s;
+ }
+ trace_create_time_ = header.ts;
+ if (FLAGS_output_time_series) {
+ time_series_start_ = header.ts;
+ }
+
+ Trace trace;
+ std::unique_ptr<TraceRecord> record;
+ while (s.ok()) {
+ trace.reset();
+ s = ReadTraceRecord(&trace);
+ if (!s.ok()) {
+ break;
+ }
+
+ end_time_ = trace.ts;
+ if (trace.type == kTraceEnd) {
+ break;
+ }
+ // Do not count TraceEnd (if there is one)
+ total_requests_++;
+
+ s = TracerHelper::DecodeTraceRecord(&trace, trace_file_version, &record);
+ if (s.IsNotSupported()) {
+ continue;
+ }
+ if (!s.ok()) {
+ return s;
+ }
+ s = record->Accept(this, nullptr);
+ if (!s.ok()) {
+ fprintf(stderr, "Cannot process the TraceRecord\n");
+ return s;
+ }
+ }
+ if (s.IsIncomplete()) {
+ // Fix it: Reaching eof returns Incomplete status at the moment.
+ return Status::OK();
+ }
+ return s;
+}
+
+// After the trace is processed by StartProcessing, the statistic data
+// is stored in the map or other in memory data structures. To get the
+// other statistic result such as key size distribution, value size
+// distribution, these data structures are re-processed here.
+Status TraceAnalyzer::MakeStatistics() {
+ int ret;
+ Status s;
+ for (int type = 0; type < kTaTypeNum; type++) {
+ if (!ta_[type].enabled) {
+ continue;
+ }
+ for (auto& stat : ta_[type].stats) {
+ stat.second.a_key_id = 0;
+ for (auto& record : stat.second.a_key_stats) {
+ record.second.key_id = stat.second.a_key_id;
+ stat.second.a_key_id++;
+ if (record.second.access_count <=
+ static_cast<uint64_t>(FLAGS_output_ignore_count)) {
+ continue;
+ }
+
+ // Generate the key access count distribution data
+ if (FLAGS_output_access_count_stats) {
+ if (stat.second.a_count_stats.find(record.second.access_count) ==
+ stat.second.a_count_stats.end()) {
+ stat.second.a_count_stats[record.second.access_count] = 1;
+ } else {
+ stat.second.a_count_stats[record.second.access_count]++;
+ }
+ }
+
+ // Generate the key size distribution data
+ if (FLAGS_output_key_distribution) {
+ if (stat.second.a_key_size_stats.find(record.first.size()) ==
+ stat.second.a_key_size_stats.end()) {
+ stat.second.a_key_size_stats[record.first.size()] = 1;
+ } else {
+ stat.second.a_key_size_stats[record.first.size()]++;
+ }
+ }
+
+ if (!FLAGS_print_correlation.empty()) {
+ s = MakeStatisticCorrelation(stat.second, record.second);
+ if (!s.ok()) {
+ return s;
+ }
+ }
+ }
+
+ // Output the prefix cut or the whole content of the accessed key space
+ if (FLAGS_output_key_stats || FLAGS_output_prefix_cut > 0) {
+ s = MakeStatisticKeyStatsOrPrefix(stat.second);
+ if (!s.ok()) {
+ return s;
+ }
+ }
+
+ // output the access count distribution
+ if (FLAGS_output_access_count_stats && stat.second.a_count_dist_f) {
+ for (auto& record : stat.second.a_count_stats) {
+ ret = snprintf(buffer_, sizeof(buffer_),
+ "access_count: %" PRIu64 " num: %" PRIu64 "\n",
+ record.first, record.second);
+ if (ret < 0) {
+ return Status::IOError("Format the output failed");
+ }
+ std::string printout(buffer_);
+ s = stat.second.a_count_dist_f->Append(printout);
+ if (!s.ok()) {
+ fprintf(stderr, "Write access count distribution file failed\n");
+ return s;
+ }
+ }
+ }
+
+ // find the medium of the key size
+ uint64_t k_count = 0;
+ bool get_mid = false;
+ for (auto& record : stat.second.a_key_size_stats) {
+ k_count += record.second;
+ if (!get_mid && k_count >= stat.second.a_key_mid) {
+ stat.second.a_key_mid = record.first;
+ get_mid = true;
+ }
+ if (FLAGS_output_key_distribution && stat.second.a_key_size_f) {
+ ret = snprintf(buffer_, sizeof(buffer_), "%" PRIu64 " %" PRIu64 "\n",
+ record.first, record.second);
+ if (ret < 0) {
+ return Status::IOError("Format output failed");
+ }
+ std::string printout(buffer_);
+ s = stat.second.a_key_size_f->Append(printout);
+ if (!s.ok()) {
+ fprintf(stderr, "Write key size distribution file failed\n");
+ return s;
+ }
+ }
+ }
+
+ // output the value size distribution
+ uint64_t v_begin = 0, v_end = 0, v_count = 0;
+ get_mid = false;
+ for (auto& record : stat.second.a_value_size_stats) {
+ v_begin = v_end;
+ v_end = (record.first + 1) * FLAGS_value_interval;
+ v_count += record.second;
+ if (!get_mid && v_count >= stat.second.a_count / 2) {
+ stat.second.a_value_mid = (v_begin + v_end) / 2;
+ get_mid = true;
+ }
+ if (FLAGS_output_value_distribution && stat.second.a_value_size_f &&
+ (type == TraceOperationType::kPut ||
+ type == TraceOperationType::kMerge)) {
+ ret = snprintf(buffer_, sizeof(buffer_),
+ "Number_of_value_size_between %" PRIu64 " and %" PRIu64
+ " is: %" PRIu64 "\n",
+ v_begin, v_end, record.second);
+ if (ret < 0) {
+ return Status::IOError("Format output failed");
+ }
+ std::string printout(buffer_);
+ s = stat.second.a_value_size_f->Append(printout);
+ if (!s.ok()) {
+ fprintf(stderr, "Write value size distribution file failed\n");
+ return s;
+ }
+ }
+ }
+ }
+ }
+
+ // Make the QPS statistics
+ if (FLAGS_output_qps_stats) {
+ s = MakeStatisticQPS();
+ if (!s.ok()) {
+ return s;
+ }
+ }
+
+ return Status::OK();
+}
+
+// Process the statistics of the key access and
+// prefix of the accessed keys if required
+Status TraceAnalyzer::MakeStatisticKeyStatsOrPrefix(TraceStats& stats) {
+ int ret;
+ Status s;
+ std::string prefix = "0";
+ uint64_t prefix_access = 0;
+ uint64_t prefix_count = 0;
+ uint64_t prefix_succ_access = 0;
+ double prefix_ave_access = 0.0;
+ stats.a_succ_count = 0;
+ for (auto& record : stats.a_key_stats) {
+ // write the key access statistic file
+ if (!stats.a_key_f) {
+ return Status::IOError("Failed to open accessed_key_stats file.");
+ }
+ stats.a_succ_count += record.second.succ_count;
+ double succ_ratio = 0.0;
+ if (record.second.access_count > 0) {
+ succ_ratio = (static_cast<double>(record.second.succ_count)) /
+ record.second.access_count;
+ }
+ ret = snprintf(buffer_, sizeof(buffer_),
+ "%u %zu %" PRIu64 " %" PRIu64 " %f\n", record.second.cf_id,
+ record.second.value_size, record.second.key_id,
+ record.second.access_count, succ_ratio);
+ if (ret < 0) {
+ return Status::IOError("Format output failed");
+ }
+ std::string printout(buffer_);
+ s = stats.a_key_f->Append(printout);
+ if (!s.ok()) {
+ fprintf(stderr, "Write key access file failed\n");
+ return s;
+ }
+
+ // write the prefix cut of the accessed keys
+ if (FLAGS_output_prefix_cut > 0 && stats.a_prefix_cut_f) {
+ if (record.first.compare(0, FLAGS_output_prefix_cut, prefix) != 0) {
+ std::string prefix_out =
+ ROCKSDB_NAMESPACE::LDBCommand::StringToHex(prefix);
+ if (prefix_count == 0) {
+ prefix_ave_access = 0.0;
+ } else {
+ prefix_ave_access =
+ (static_cast<double>(prefix_access)) / prefix_count;
+ }
+ double prefix_succ_ratio = 0.0;
+ if (prefix_access > 0) {
+ prefix_succ_ratio =
+ (static_cast<double>(prefix_succ_access)) / prefix_access;
+ }
+ ret =
+ snprintf(buffer_, sizeof(buffer_),
+ "%" PRIu64 " %" PRIu64 " %" PRIu64 " %f %f %s\n",
+ record.second.key_id, prefix_access, prefix_count,
+ prefix_ave_access, prefix_succ_ratio, prefix_out.c_str());
+ if (ret < 0) {
+ return Status::IOError("Format output failed");
+ }
+ std::string pout(buffer_);
+ s = stats.a_prefix_cut_f->Append(pout);
+ if (!s.ok()) {
+ fprintf(stderr, "Write accessed key prefix file failed\n");
+ return s;
+ }
+
+ // make the top k statistic for the prefix
+ if (static_cast<int32_t>(stats.top_k_prefix_access.size()) <
+ FLAGS_print_top_k_access) {
+ stats.top_k_prefix_access.push(
+ std::make_pair(prefix_access, prefix_out));
+ } else {
+ if (prefix_access > stats.top_k_prefix_access.top().first) {
+ stats.top_k_prefix_access.pop();
+ stats.top_k_prefix_access.push(
+ std::make_pair(prefix_access, prefix_out));
+ }
+ }
+
+ if (static_cast<int32_t>(stats.top_k_prefix_ave.size()) <
+ FLAGS_print_top_k_access) {
+ stats.top_k_prefix_ave.push(
+ std::make_pair(prefix_ave_access, prefix_out));
+ } else {
+ if (prefix_ave_access > stats.top_k_prefix_ave.top().first) {
+ stats.top_k_prefix_ave.pop();
+ stats.top_k_prefix_ave.push(
+ std::make_pair(prefix_ave_access, prefix_out));
+ }
+ }
+
+ prefix = record.first.substr(0, FLAGS_output_prefix_cut);
+ prefix_access = 0;
+ prefix_count = 0;
+ prefix_succ_access = 0;
+ }
+ prefix_access += record.second.access_count;
+ prefix_count += 1;
+ prefix_succ_access += record.second.succ_count;
+ }
+ }
+ return Status::OK();
+}
+
+// Process the statistics of different query type
+// correlations
+Status TraceAnalyzer::MakeStatisticCorrelation(TraceStats& stats,
+ StatsUnit& unit) {
+ if (stats.correlation_output.size() !=
+ analyzer_opts_.correlation_list.size()) {
+ return Status::Corruption("Cannot make the statistic of correlation.");
+ }
+
+ for (int i = 0; i < static_cast<int>(analyzer_opts_.correlation_list.size());
+ i++) {
+ if (i >= static_cast<int>(stats.correlation_output.size()) ||
+ i >= static_cast<int>(unit.v_correlation.size())) {
+ break;
+ }
+ stats.correlation_output[i].first += unit.v_correlation[i].count;
+ stats.correlation_output[i].second += unit.v_correlation[i].total_ts;
+ }
+ return Status::OK();
+}
+
+// Process the statistics of QPS
+Status TraceAnalyzer::MakeStatisticQPS() {
+ if (begin_time_ == 0) {
+ begin_time_ = trace_create_time_;
+ }
+ uint32_t duration =
+ static_cast<uint32_t>((end_time_ - begin_time_) / 1000000);
+ int ret;
+ Status s;
+ std::vector<std::vector<uint32_t>> type_qps(
+ duration, std::vector<uint32_t>(kTaTypeNum + 1, 0));
+ std::vector<uint64_t> qps_sum(kTaTypeNum + 1, 0);
+ std::vector<uint32_t> qps_peak(kTaTypeNum + 1, 0);
+ qps_ave_.resize(kTaTypeNum + 1);
+
+ for (int type = 0; type < kTaTypeNum; type++) {
+ if (!ta_[type].enabled) {
+ continue;
+ }
+ for (auto& stat : ta_[type].stats) {
+ uint32_t time_line = 0;
+ uint64_t cf_qps_sum = 0;
+ for (auto& time_it : stat.second.a_qps_stats) {
+ if (time_it.first >= duration) {
+ continue;
+ }
+ type_qps[time_it.first][kTaTypeNum] += time_it.second;
+ type_qps[time_it.first][type] += time_it.second;
+ cf_qps_sum += time_it.second;
+ if (time_it.second > stat.second.a_peak_qps) {
+ stat.second.a_peak_qps = time_it.second;
+ }
+ if (stat.second.a_qps_f) {
+ while (time_line < time_it.first) {
+ ret = snprintf(buffer_, sizeof(buffer_), "%u\n", 0);
+ if (ret < 0) {
+ return Status::IOError("Format the output failed");
+ }
+ std::string printout(buffer_);
+ s = stat.second.a_qps_f->Append(printout);
+ if (!s.ok()) {
+ fprintf(stderr, "Write QPS file failed\n");
+ return s;
+ }
+ time_line++;
+ }
+ ret = snprintf(buffer_, sizeof(buffer_), "%u\n", time_it.second);
+ if (ret < 0) {
+ return Status::IOError("Format the output failed");
+ }
+ std::string printout(buffer_);
+ s = stat.second.a_qps_f->Append(printout);
+ if (!s.ok()) {
+ fprintf(stderr, "Write QPS file failed\n");
+ return s;
+ }
+ if (time_line == time_it.first) {
+ time_line++;
+ }
+ }
+
+ // Process the top k QPS peaks
+ if (FLAGS_output_prefix_cut > 0) {
+ if (static_cast<int32_t>(stat.second.top_k_qps_sec.size()) <
+ FLAGS_print_top_k_access) {
+ stat.second.top_k_qps_sec.push(
+ std::make_pair(time_it.second, time_it.first));
+ } else {
+ if (stat.second.top_k_qps_sec.size() > 0 &&
+ stat.second.top_k_qps_sec.top().first < time_it.second) {
+ stat.second.top_k_qps_sec.pop();
+ stat.second.top_k_qps_sec.push(
+ std::make_pair(time_it.second, time_it.first));
+ }
+ }
+ }
+ }
+ if (duration == 0) {
+ stat.second.a_ave_qps = 0;
+ } else {
+ stat.second.a_ave_qps = (static_cast<double>(cf_qps_sum)) / duration;
+ }
+
+ // Output the accessed unique key number change overtime
+ if (stat.second.a_key_num_f) {
+ uint64_t cur_uni_key =
+ static_cast<uint64_t>(stat.second.a_key_stats.size());
+ double cur_ratio = 0.0;
+ uint64_t cur_num = 0;
+ for (uint32_t i = 0; i < duration; i++) {
+ auto find_time = stat.second.uni_key_num.find(i);
+ if (find_time != stat.second.uni_key_num.end()) {
+ cur_ratio = (static_cast<double>(find_time->second)) / cur_uni_key;
+ cur_num = find_time->second;
+ }
+ ret = snprintf(buffer_, sizeof(buffer_), "%" PRIu64 " %.12f\n",
+ cur_num, cur_ratio);
+ if (ret < 0) {
+ return Status::IOError("Format the output failed");
+ }
+ std::string printout(buffer_);
+ s = stat.second.a_key_num_f->Append(printout);
+ if (!s.ok()) {
+ fprintf(stderr,
+ "Write accessed unique key number change file failed\n");
+ return s;
+ }
+ }
+ }
+
+ // output the prefix of top k access peak
+ if (FLAGS_output_prefix_cut > 0 && stat.second.a_top_qps_prefix_f) {
+ while (!stat.second.top_k_qps_sec.empty()) {
+ ret = snprintf(buffer_, sizeof(buffer_), "At time: %u with QPS: %u\n",
+ stat.second.top_k_qps_sec.top().second,
+ stat.second.top_k_qps_sec.top().first);
+ if (ret < 0) {
+ return Status::IOError("Format the output failed");
+ }
+ std::string printout(buffer_);
+ s = stat.second.a_top_qps_prefix_f->Append(printout);
+ if (!s.ok()) {
+ fprintf(stderr, "Write prefix QPS top K file failed\n");
+ return s;
+ }
+ uint32_t qps_time = stat.second.top_k_qps_sec.top().second;
+ stat.second.top_k_qps_sec.pop();
+ if (stat.second.a_qps_prefix_stats.find(qps_time) !=
+ stat.second.a_qps_prefix_stats.end()) {
+ for (auto& qps_prefix : stat.second.a_qps_prefix_stats[qps_time]) {
+ std::string qps_prefix_out =
+ ROCKSDB_NAMESPACE::LDBCommand::StringToHex(qps_prefix.first);
+ ret = snprintf(buffer_, sizeof(buffer_),
+ "The prefix: %s Access count: %u\n",
+ qps_prefix_out.c_str(), qps_prefix.second);
+ if (ret < 0) {
+ return Status::IOError("Format the output failed");
+ }
+ std::string pout(buffer_);
+ s = stat.second.a_top_qps_prefix_f->Append(pout);
+ if (!s.ok()) {
+ fprintf(stderr, "Write prefix QPS top K file failed\n");
+ return s;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (qps_f_) {
+ for (uint32_t i = 0; i < duration; i++) {
+ for (int type = 0; type <= kTaTypeNum; type++) {
+ if (type < kTaTypeNum) {
+ ret = snprintf(buffer_, sizeof(buffer_), "%u ", type_qps[i][type]);
+ } else {
+ ret = snprintf(buffer_, sizeof(buffer_), "%u\n", type_qps[i][type]);
+ }
+ if (ret < 0) {
+ return Status::IOError("Format the output failed");
+ }
+ std::string printout(buffer_);
+ s = qps_f_->Append(printout);
+ if (!s.ok()) {
+ return s;
+ }
+ qps_sum[type] += type_qps[i][type];
+ if (type_qps[i][type] > qps_peak[type]) {
+ qps_peak[type] = type_qps[i][type];
+ }
+ }
+ }
+ }
+
+ if (cf_qps_f_) {
+ int cfs_size = static_cast<uint32_t>(cfs_.size());
+ uint32_t v;
+ for (uint32_t i = 0; i < duration; i++) {
+ for (int cf = 0; cf < cfs_size; cf++) {
+ if (cfs_[cf].cf_qps.find(i) != cfs_[cf].cf_qps.end()) {
+ v = cfs_[cf].cf_qps[i];
+ } else {
+ v = 0;
+ }
+ if (cf < cfs_size - 1) {
+ ret = snprintf(buffer_, sizeof(buffer_), "%u ", v);
+ } else {
+ ret = snprintf(buffer_, sizeof(buffer_), "%u\n", v);
+ }
+ if (ret < 0) {
+ return Status::IOError("Format the output failed");
+ }
+ std::string printout(buffer_);
+ s = cf_qps_f_->Append(printout);
+ if (!s.ok()) {
+ return s;
+ }
+ }
+ }
+ }
+
+ qps_peak_ = qps_peak;
+ for (int type = 0; type <= kTaTypeNum; type++) {
+ if (duration == 0) {
+ qps_ave_[type] = 0;
+ } else {
+ qps_ave_[type] = (static_cast<double>(qps_sum[type])) / duration;
+ }
+ }
+
+ return Status::OK();
+}
+
+// In reprocessing, if we have the whole key space
+// we can output the access count of all keys in a cf
+// we can make some statistics of the whole key space
+// also, we output the top k accessed keys here
+Status TraceAnalyzer::ReProcessing() {
+ int ret;
+ Status s;
+ for (auto& cf_it : cfs_) {
+ uint32_t cf_id = cf_it.first;
+
+ // output the time series;
+ if (FLAGS_output_time_series) {
+ for (int type = 0; type < kTaTypeNum; type++) {
+ if (!ta_[type].enabled ||
+ ta_[type].stats.find(cf_id) == ta_[type].stats.end()) {
+ continue;
+ }
+ TraceStats& stat = ta_[type].stats[cf_id];
+ if (!stat.time_series_f) {
+ fprintf(stderr, "Cannot write time_series of '%s' in '%u'\n",
+ ta_[type].type_name.c_str(), cf_id);
+ continue;
+ }
+ while (!stat.time_series.empty()) {
+ uint64_t key_id = 0;
+ auto found = stat.a_key_stats.find(stat.time_series.front().key);
+ if (found != stat.a_key_stats.end()) {
+ key_id = found->second.key_id;
+ }
+ ret =
+ snprintf(buffer_, sizeof(buffer_), "%u %" PRIu64 " %" PRIu64 "\n",
+ stat.time_series.front().type,
+ stat.time_series.front().ts, key_id);
+ if (ret < 0) {
+ return Status::IOError("Format the output failed");
+ }
+ std::string printout(buffer_);
+ s = stat.time_series_f->Append(printout);
+ if (!s.ok()) {
+ fprintf(stderr, "Write time series file failed\n");
+ return s;
+ }
+ stat.time_series.pop_front();
+ }
+ }
+ }
+
+ // process the whole key space if needed
+ if (!FLAGS_key_space_dir.empty()) {
+ std::string whole_key_path =
+ FLAGS_key_space_dir + "/" + std::to_string(cf_id) + ".txt";
+ std::string input_key, get_key;
+ std::vector<std::string> prefix(kTaTypeNum);
+ std::unique_ptr<FSSequentialFile> file;
+
+ s = env_->GetFileSystem()->NewSequentialFile(
+ whole_key_path, FileOptions(env_options_), &file, nullptr);
+ if (!s.ok()) {
+ fprintf(stderr, "Cannot open the whole key space file of CF: %u\n",
+ cf_id);
+ file.reset();
+ }
+
+ if (file) {
+ size_t kTraceFileReadaheadSize = 2 * 1024 * 1024;
+ LineFileReader lf_reader(
+ std::move(file), whole_key_path,
+ kTraceFileReadaheadSize /* filereadahead_size */);
+ for (cfs_[cf_id].w_count = 0; lf_reader.ReadLine(
+ &get_key, Env::IO_TOTAL /* rate_limiter_priority */);
+ ++cfs_[cf_id].w_count) {
+ input_key = ROCKSDB_NAMESPACE::LDBCommand::HexToString(get_key);
+ for (int type = 0; type < kTaTypeNum; type++) {
+ if (!ta_[type].enabled) {
+ continue;
+ }
+ TraceStats& stat = ta_[type].stats[cf_id];
+ if (stat.w_key_f) {
+ if (stat.a_key_stats.find(input_key) != stat.a_key_stats.end()) {
+ ret = snprintf(buffer_, sizeof(buffer_),
+ "%" PRIu64 " %" PRIu64 "\n", cfs_[cf_id].w_count,
+ stat.a_key_stats[input_key].access_count);
+ if (ret < 0) {
+ return Status::IOError("Format the output failed");
+ }
+ std::string printout(buffer_);
+ s = stat.w_key_f->Append(printout);
+ if (!s.ok()) {
+ fprintf(stderr, "Write whole key space access file failed\n");
+ return s;
+ }
+ }
+ }
+
+ // Output the prefix cut file of the whole key space
+ if (FLAGS_output_prefix_cut > 0 && stat.w_prefix_cut_f) {
+ if (input_key.compare(0, FLAGS_output_prefix_cut, prefix[type]) !=
+ 0) {
+ prefix[type] = input_key.substr(0, FLAGS_output_prefix_cut);
+ std::string prefix_out =
+ ROCKSDB_NAMESPACE::LDBCommand::StringToHex(prefix[type]);
+ ret = snprintf(buffer_, sizeof(buffer_), "%" PRIu64 " %s\n",
+ cfs_[cf_id].w_count, prefix_out.c_str());
+ if (ret < 0) {
+ return Status::IOError("Format the output failed");
+ }
+ std::string printout(buffer_);
+ s = stat.w_prefix_cut_f->Append(printout);
+ if (!s.ok()) {
+ fprintf(stderr,
+ "Write whole key space prefix cut file failed\n");
+ return s;
+ }
+ }
+ }
+ }
+
+ // Make the statistics fo the key size distribution
+ if (FLAGS_output_key_distribution) {
+ if (cfs_[cf_id].w_key_size_stats.find(input_key.size()) ==
+ cfs_[cf_id].w_key_size_stats.end()) {
+ cfs_[cf_id].w_key_size_stats[input_key.size()] = 1;
+ } else {
+ cfs_[cf_id].w_key_size_stats[input_key.size()]++;
+ }
+ }
+ }
+ s = lf_reader.GetStatus();
+ if (!s.ok()) {
+ fprintf(stderr, "Read whole key space file failed\n");
+ return s;
+ }
+ }
+ }
+
+ // process the top k accessed keys
+ if (FLAGS_print_top_k_access > 0) {
+ for (int type = 0; type < kTaTypeNum; type++) {
+ if (!ta_[type].enabled ||
+ ta_[type].stats.find(cf_id) == ta_[type].stats.end()) {
+ continue;
+ }
+ TraceStats& stat = ta_[type].stats[cf_id];
+ for (auto& record : stat.a_key_stats) {
+ if (static_cast<int32_t>(stat.top_k_queue.size()) <
+ FLAGS_print_top_k_access) {
+ stat.top_k_queue.push(
+ std::make_pair(record.second.access_count, record.first));
+ } else {
+ if (record.second.access_count > stat.top_k_queue.top().first) {
+ stat.top_k_queue.pop();
+ stat.top_k_queue.push(
+ std::make_pair(record.second.access_count, record.first));
+ }
+ }
+ }
+ }
+ }
+ }
+ return Status::OK();
+}
+
+// End the processing, print the requested results
+Status TraceAnalyzer::EndProcessing() {
+ Status s;
+ if (trace_sequence_f_) {
+ s = trace_sequence_f_->Close();
+ }
+ if (FLAGS_no_print) {
+ return s;
+ }
+ PrintStatistics();
+ if (s.ok()) {
+ s = CloseOutputFiles();
+ }
+ return s;
+}
+
+// Insert the corresponding key statistics to the correct type
+// and correct CF, output the time-series file if needed
+Status TraceAnalyzer::KeyStatsInsertion(const uint32_t& type,
+ const uint32_t& cf_id,
+ const std::string& key,
+ const size_t value_size,
+ const uint64_t ts) {
+ Status s;
+ StatsUnit unit;
+ unit.key_id = 0;
+ unit.cf_id = cf_id;
+ unit.value_size = value_size;
+ unit.access_count = 1;
+ unit.latest_ts = ts;
+ if ((type != TraceOperationType::kGet &&
+ type != TraceOperationType::kMultiGet) ||
+ value_size > 0) {
+ unit.succ_count = 1;
+ } else {
+ unit.succ_count = 0;
+ }
+ unit.v_correlation.resize(analyzer_opts_.correlation_list.size());
+ for (int i = 0;
+ i < (static_cast<int>(analyzer_opts_.correlation_list.size())); i++) {
+ unit.v_correlation[i].count = 0;
+ unit.v_correlation[i].total_ts = 0;
+ }
+ std::string prefix;
+ if (FLAGS_output_prefix_cut > 0) {
+ prefix = key.substr(0, FLAGS_output_prefix_cut);
+ }
+
+ if (begin_time_ == 0) {
+ begin_time_ = ts;
+ }
+ uint32_t time_in_sec;
+ if (ts < begin_time_) {
+ time_in_sec = 0;
+ } else {
+ time_in_sec = static_cast<uint32_t>((ts - begin_time_) / 1000000);
+ }
+
+ uint64_t dist_value_size = value_size / FLAGS_value_interval;
+ auto found_stats = ta_[type].stats.find(cf_id);
+ if (found_stats == ta_[type].stats.end()) {
+ ta_[type].stats[cf_id].cf_id = cf_id;
+ ta_[type].stats[cf_id].cf_name = std::to_string(cf_id);
+ ta_[type].stats[cf_id].a_count = 1;
+ ta_[type].stats[cf_id].a_key_id = 0;
+ ta_[type].stats[cf_id].a_key_size_sqsum = MultiplyCheckOverflow(
+ static_cast<uint64_t>(key.size()), static_cast<uint64_t>(key.size()));
+ ta_[type].stats[cf_id].a_key_size_sum = key.size();
+ ta_[type].stats[cf_id].a_value_size_sqsum = MultiplyCheckOverflow(
+ static_cast<uint64_t>(value_size), static_cast<uint64_t>(value_size));
+ ta_[type].stats[cf_id].a_value_size_sum = value_size;
+ s = OpenStatsOutputFiles(ta_[type].type_name, ta_[type].stats[cf_id]);
+ if (!FLAGS_print_correlation.empty()) {
+ s = StatsUnitCorrelationUpdate(unit, type, ts, key);
+ }
+ ta_[type].stats[cf_id].a_key_stats[key] = unit;
+ ta_[type].stats[cf_id].a_value_size_stats[dist_value_size] = 1;
+ ta_[type].stats[cf_id].a_qps_stats[time_in_sec] = 1;
+ ta_[type].stats[cf_id].correlation_output.resize(
+ analyzer_opts_.correlation_list.size());
+ if (FLAGS_output_prefix_cut > 0) {
+ std::map<std::string, uint32_t> tmp_qps_map;
+ tmp_qps_map[prefix] = 1;
+ ta_[type].stats[cf_id].a_qps_prefix_stats[time_in_sec] = tmp_qps_map;
+ }
+ if (time_in_sec != cur_time_sec_) {
+ ta_[type].stats[cf_id].uni_key_num[cur_time_sec_] =
+ static_cast<uint64_t>(ta_[type].stats[cf_id].a_key_stats.size());
+ cur_time_sec_ = time_in_sec;
+ }
+ } else {
+ found_stats->second.a_count++;
+ found_stats->second.a_key_size_sqsum += MultiplyCheckOverflow(
+ static_cast<uint64_t>(key.size()), static_cast<uint64_t>(key.size()));
+ found_stats->second.a_key_size_sum += key.size();
+ found_stats->second.a_value_size_sqsum += MultiplyCheckOverflow(
+ static_cast<uint64_t>(value_size), static_cast<uint64_t>(value_size));
+ found_stats->second.a_value_size_sum += value_size;
+ auto found_key = found_stats->second.a_key_stats.find(key);
+ if (found_key == found_stats->second.a_key_stats.end()) {
+ found_stats->second.a_key_stats[key] = unit;
+ } else {
+ found_key->second.access_count++;
+ if (type != TraceOperationType::kGet || value_size > 0) {
+ found_key->second.succ_count++;
+ }
+ if (!FLAGS_print_correlation.empty()) {
+ s = StatsUnitCorrelationUpdate(found_key->second, type, ts, key);
+ }
+ }
+ if (time_in_sec != cur_time_sec_) {
+ found_stats->second.uni_key_num[cur_time_sec_] =
+ static_cast<uint64_t>(found_stats->second.a_key_stats.size());
+ cur_time_sec_ = time_in_sec;
+ }
+
+ auto found_value =
+ found_stats->second.a_value_size_stats.find(dist_value_size);
+ if (found_value == found_stats->second.a_value_size_stats.end()) {
+ found_stats->second.a_value_size_stats[dist_value_size] = 1;
+ } else {
+ found_value->second++;
+ }
+
+ auto found_qps = found_stats->second.a_qps_stats.find(time_in_sec);
+ if (found_qps == found_stats->second.a_qps_stats.end()) {
+ found_stats->second.a_qps_stats[time_in_sec] = 1;
+ } else {
+ found_qps->second++;
+ }
+
+ if (FLAGS_output_prefix_cut > 0) {
+ auto found_qps_prefix =
+ found_stats->second.a_qps_prefix_stats.find(time_in_sec);
+ if (found_qps_prefix == found_stats->second.a_qps_prefix_stats.end()) {
+ std::map<std::string, uint32_t> tmp_qps_map;
+ found_stats->second.a_qps_prefix_stats[time_in_sec] = tmp_qps_map;
+ }
+ if (found_stats->second.a_qps_prefix_stats[time_in_sec].find(prefix) ==
+ found_stats->second.a_qps_prefix_stats[time_in_sec].end()) {
+ found_stats->second.a_qps_prefix_stats[time_in_sec][prefix] = 1;
+ } else {
+ found_stats->second.a_qps_prefix_stats[time_in_sec][prefix]++;
+ }
+ }
+ }
+
+ if (cfs_.find(cf_id) == cfs_.end()) {
+ CfUnit cf_unit;
+ cf_unit.cf_id = cf_id;
+ cf_unit.w_count = 0;
+ cf_unit.a_count = 0;
+ cfs_[cf_id] = cf_unit;
+ }
+
+ if (FLAGS_output_qps_stats) {
+ cfs_[cf_id].cf_qps[time_in_sec]++;
+ }
+
+ if (FLAGS_output_time_series) {
+ TraceUnit trace_u;
+ trace_u.type = type;
+ trace_u.key = key;
+ trace_u.value_size = value_size;
+ trace_u.ts = (ts - time_series_start_) / 1000000;
+ trace_u.cf_id = cf_id;
+ ta_[type].stats[cf_id].time_series.push_back(trace_u);
+ }
+
+ return s;
+}
+
+// Update the correlation unit of each key if enabled
+Status TraceAnalyzer::StatsUnitCorrelationUpdate(StatsUnit& unit,
+ const uint32_t& type_second,
+ const uint64_t& ts,
+ const std::string& key) {
+ if (type_second >= kTaTypeNum) {
+ fprintf(stderr, "Unknown Type Id: %u\n", type_second);
+ return Status::NotFound();
+ }
+
+ for (int type_first = 0; type_first < kTaTypeNum; type_first++) {
+ if (type_first >= static_cast<int>(ta_.size()) ||
+ type_first >= static_cast<int>(analyzer_opts_.correlation_map.size())) {
+ break;
+ }
+ if (analyzer_opts_.correlation_map[type_first][type_second] < 0 ||
+ ta_[type_first].stats.find(unit.cf_id) == ta_[type_first].stats.end() ||
+ ta_[type_first].stats[unit.cf_id].a_key_stats.find(key) ==
+ ta_[type_first].stats[unit.cf_id].a_key_stats.end() ||
+ ta_[type_first].stats[unit.cf_id].a_key_stats[key].latest_ts == ts) {
+ continue;
+ }
+
+ int correlation_id =
+ analyzer_opts_.correlation_map[type_first][type_second];
+
+ // after get the x-y operation time or x, update;
+ if (correlation_id < 0 ||
+ correlation_id >= static_cast<int>(unit.v_correlation.size())) {
+ continue;
+ }
+ unit.v_correlation[correlation_id].count++;
+ unit.v_correlation[correlation_id].total_ts +=
+ (ts - ta_[type_first].stats[unit.cf_id].a_key_stats[key].latest_ts);
+ }
+
+ unit.latest_ts = ts;
+ return Status::OK();
+}
+
+// when a new trace statistic is created, the file handler
+// pointers should be initiated if needed according to
+// the trace analyzer options
+Status TraceAnalyzer::OpenStatsOutputFiles(const std::string& type,
+ TraceStats& new_stats) {
+ Status s;
+ if (FLAGS_output_key_stats) {
+ s = CreateOutputFile(type, new_stats.cf_name, "accessed_key_stats.txt",
+ &new_stats.a_key_f);
+ s = CreateOutputFile(type, new_stats.cf_name,
+ "accessed_unique_key_num_change.txt",
+ &new_stats.a_key_num_f);
+ if (!FLAGS_key_space_dir.empty()) {
+ s = CreateOutputFile(type, new_stats.cf_name, "whole_key_stats.txt",
+ &new_stats.w_key_f);
+ }
+ }
+
+ if (FLAGS_output_access_count_stats) {
+ s = CreateOutputFile(type, new_stats.cf_name,
+ "accessed_key_count_distribution.txt",
+ &new_stats.a_count_dist_f);
+ }
+
+ if (FLAGS_output_prefix_cut > 0) {
+ s = CreateOutputFile(type, new_stats.cf_name, "accessed_key_prefix_cut.txt",
+ &new_stats.a_prefix_cut_f);
+ if (!FLAGS_key_space_dir.empty()) {
+ s = CreateOutputFile(type, new_stats.cf_name, "whole_key_prefix_cut.txt",
+ &new_stats.w_prefix_cut_f);
+ }
+
+ if (FLAGS_output_qps_stats) {
+ s = CreateOutputFile(type, new_stats.cf_name,
+ "accessed_top_k_qps_prefix_cut.txt",
+ &new_stats.a_top_qps_prefix_f);
+ }
+ }
+
+ if (FLAGS_output_time_series) {
+ s = CreateOutputFile(type, new_stats.cf_name, "time_series.txt",
+ &new_stats.time_series_f);
+ }
+
+ if (FLAGS_output_value_distribution) {
+ s = CreateOutputFile(type, new_stats.cf_name,
+ "accessed_value_size_distribution.txt",
+ &new_stats.a_value_size_f);
+ }
+
+ if (FLAGS_output_key_distribution) {
+ s = CreateOutputFile(type, new_stats.cf_name,
+ "accessed_key_size_distribution.txt",
+ &new_stats.a_key_size_f);
+ }
+
+ if (FLAGS_output_qps_stats) {
+ s = CreateOutputFile(type, new_stats.cf_name, "qps_stats.txt",
+ &new_stats.a_qps_f);
+ }
+
+ return s;
+}
+
+// create the output path of the files to be opened
+Status TraceAnalyzer::CreateOutputFile(
+ const std::string& type, const std::string& cf_name,
+ const std::string& ending,
+ std::unique_ptr<ROCKSDB_NAMESPACE::WritableFile>* f_ptr) {
+ std::string path;
+ path = output_path_ + "/" + FLAGS_output_prefix + "-" + type + "-" + cf_name +
+ "-" + ending;
+ Status s;
+ s = env_->NewWritableFile(path, f_ptr, env_options_);
+ if (!s.ok()) {
+ fprintf(stderr, "Cannot open file: %s\n", path.c_str());
+ exit(1);
+ }
+ return Status::OK();
+}
+
+// Close the output files in the TraceStats if they are opened
+Status TraceAnalyzer::CloseOutputFiles() {
+ Status s;
+ for (int type = 0; type < kTaTypeNum; type++) {
+ if (!ta_[type].enabled) {
+ continue;
+ }
+ for (auto& stat : ta_[type].stats) {
+ if (s.ok() && stat.second.time_series_f) {
+ s = stat.second.time_series_f->Close();
+ }
+
+ if (s.ok() && stat.second.a_key_f) {
+ s = stat.second.a_key_f->Close();
+ }
+
+ if (s.ok() && stat.second.a_key_num_f) {
+ s = stat.second.a_key_num_f->Close();
+ }
+
+ if (s.ok() && stat.second.a_count_dist_f) {
+ s = stat.second.a_count_dist_f->Close();
+ }
+
+ if (s.ok() && stat.second.a_prefix_cut_f) {
+ s = stat.second.a_prefix_cut_f->Close();
+ }
+
+ if (s.ok() && stat.second.a_value_size_f) {
+ s = stat.second.a_value_size_f->Close();
+ }
+
+ if (s.ok() && stat.second.a_key_size_f) {
+ s = stat.second.a_key_size_f->Close();
+ }
+
+ if (s.ok() && stat.second.a_qps_f) {
+ s = stat.second.a_qps_f->Close();
+ }
+
+ if (s.ok() && stat.second.a_top_qps_prefix_f) {
+ s = stat.second.a_top_qps_prefix_f->Close();
+ }
+
+ if (s.ok() && stat.second.w_key_f) {
+ s = stat.second.w_key_f->Close();
+ }
+ if (s.ok() && stat.second.w_prefix_cut_f) {
+ s = stat.second.w_prefix_cut_f->Close();
+ }
+ }
+ }
+ return s;
+}
+
+Status TraceAnalyzer::Handle(const WriteQueryTraceRecord& record,
+ std::unique_ptr<TraceRecordResult>* /*result*/) {
+ total_writes_++;
+ // Note that, if the write happens in a transaction,
+ // 'Write' will be called twice, one for Prepare, one for
+ // Commit. Thus, in the trace, for the same WriteBatch, there
+ // will be two records if it is in a transaction. Here, we only
+ // process the reord that is committed. If write is non-transaction,
+ // HasBeginPrepare()==false, so we process it normally.
+ WriteBatch batch(record.GetWriteBatchRep().ToString());
+ if (batch.Count() == 0 || (batch.HasBeginPrepare() && !batch.HasCommit())) {
+ return Status::OK();
+ }
+ write_batch_ts_ = record.GetTimestamp();
+
+ // write_result_ will be updated in batch's handler during iteration.
+ Status s = batch.Iterate(this);
+ write_batch_ts_ = 0;
+ if (!s.ok()) {
+ fprintf(stderr, "Cannot process the write batch in the trace\n");
+ return s;
+ }
+
+ return Status::OK();
+}
+
+Status TraceAnalyzer::Handle(const GetQueryTraceRecord& record,
+ std::unique_ptr<TraceRecordResult>* /*result*/) {
+ total_gets_++;
+ return OutputAnalysisResult(TraceOperationType::kGet, record.GetTimestamp(),
+ record.GetColumnFamilyID(),
+ std::move(record.GetKey()), 0);
+}
+
+Status TraceAnalyzer::Handle(const IteratorSeekQueryTraceRecord& record,
+ std::unique_ptr<TraceRecordResult>* /*result*/) {
+ TraceOperationType op_type;
+ if (record.GetSeekType() == IteratorSeekQueryTraceRecord::kSeek) {
+ op_type = TraceOperationType::kIteratorSeek;
+ total_seeks_++;
+ } else {
+ op_type = TraceOperationType::kIteratorSeekForPrev;
+ total_seek_prevs_++;
+ }
+
+ // To do: shall we add lower/upper bounds?
+
+ return OutputAnalysisResult(op_type, record.GetTimestamp(),
+ record.GetColumnFamilyID(),
+ std::move(record.GetKey()), 0);
+}
+
+Status TraceAnalyzer::Handle(const MultiGetQueryTraceRecord& record,
+ std::unique_ptr<TraceRecordResult>* /*result*/) {
+ total_multigets_++;
+
+ std::vector<uint32_t> cf_ids = record.GetColumnFamilyIDs();
+ std::vector<Slice> keys = record.GetKeys();
+ std::vector<size_t> value_sizes;
+
+ // If the size does not match is not the error of tracing and anayzing, we
+ // just report it to the user. The analyzing continues.
+ if (cf_ids.size() > keys.size()) {
+ printf("The CF ID vector size does not match the keys vector size!\n");
+ // Make the sure the 2 vectors are of the same (smaller) size.
+ cf_ids.resize(keys.size());
+ } else if (cf_ids.size() < keys.size()) {
+ printf("The CF ID vector size does not match the keys vector size!\n");
+ // Make the sure the 2 vectors are of the same (smaller) size.
+ keys.resize(cf_ids.size());
+ }
+ // Now the 2 vectors must be of the same size.
+ value_sizes.resize(cf_ids.size(), 0);
+
+ return OutputAnalysisResult(TraceOperationType::kMultiGet,
+ record.GetTimestamp(), std::move(cf_ids),
+ std::move(keys), std::move(value_sizes));
+}
+
+// Handle the Put request in the write batch of the trace
+Status TraceAnalyzer::PutCF(uint32_t column_family_id, const Slice& key,
+ const Slice& value) {
+ return OutputAnalysisResult(TraceOperationType::kPut, write_batch_ts_,
+ column_family_id, key, value.size());
+}
+
+// Handle the Delete request in the write batch of the trace
+Status TraceAnalyzer::DeleteCF(uint32_t column_family_id, const Slice& key) {
+ return OutputAnalysisResult(TraceOperationType::kDelete, write_batch_ts_,
+ column_family_id, key, 0);
+}
+
+// Handle the SingleDelete request in the write batch of the trace
+Status TraceAnalyzer::SingleDeleteCF(uint32_t column_family_id,
+ const Slice& key) {
+ return OutputAnalysisResult(TraceOperationType::kSingleDelete,
+ write_batch_ts_, column_family_id, key, 0);
+}
+
+// Handle the DeleteRange request in the write batch of the trace
+Status TraceAnalyzer::DeleteRangeCF(uint32_t column_family_id,
+ const Slice& begin_key,
+ const Slice& end_key) {
+ return OutputAnalysisResult(TraceOperationType::kRangeDelete, write_batch_ts_,
+ {column_family_id, column_family_id},
+ {begin_key, end_key}, {0, 0});
+}
+
+// Handle the Merge request in the write batch of the trace
+Status TraceAnalyzer::MergeCF(uint32_t column_family_id, const Slice& key,
+ const Slice& value) {
+ return OutputAnalysisResult(TraceOperationType::kMerge, write_batch_ts_,
+ column_family_id, key, value.size());
+}
+
+Status TraceAnalyzer::OutputAnalysisResult(TraceOperationType op_type,
+ uint64_t timestamp,
+ std::vector<uint32_t> cf_ids,
+ std::vector<Slice> keys,
+ std::vector<size_t> value_sizes) {
+ assert(!cf_ids.empty());
+ assert(cf_ids.size() == keys.size());
+ assert(cf_ids.size() == value_sizes.size());
+
+ Status s;
+
+ if (FLAGS_convert_to_human_readable_trace && trace_sequence_f_) {
+ // DeleteRane only writes the begin_key.
+ size_t cnt =
+ op_type == TraceOperationType::kRangeDelete ? 1 : cf_ids.size();
+ for (size_t i = 0; i < cnt; i++) {
+ s = WriteTraceSequence(op_type, cf_ids[i], keys[i], value_sizes[i],
+ timestamp);
+ if (!s.ok()) {
+ return Status::Corruption("Failed to write the trace sequence to file");
+ }
+ }
+ }
+
+ if (ta_[op_type].sample_count >= sample_max_) {
+ ta_[op_type].sample_count = 0;
+ }
+ if (ta_[op_type].sample_count > 0) {
+ ta_[op_type].sample_count++;
+ return Status::OK();
+ }
+ ta_[op_type].sample_count++;
+
+ if (!ta_[op_type].enabled) {
+ return Status::OK();
+ }
+
+ for (size_t i = 0; i < cf_ids.size(); i++) {
+ // Get query does not have value part, just give a fixed value 10 for easy
+ // calculation.
+ s = KeyStatsInsertion(
+ op_type, cf_ids[i], keys[i].ToString(),
+ value_sizes[i] == 0 ? kShadowValueSize : value_sizes[i], timestamp);
+ if (!s.ok()) {
+ return Status::Corruption("Failed to insert key statistics");
+ }
+ }
+
+ return Status::OK();
+}
+
+Status TraceAnalyzer::OutputAnalysisResult(TraceOperationType op_type,
+ uint64_t timestamp, uint32_t cf_id,
+ const Slice& key,
+ size_t value_size) {
+ return OutputAnalysisResult(
+ op_type, timestamp, std::vector<uint32_t>({cf_id}),
+ std::vector<Slice>({key}), std::vector<size_t>({value_size}));
+}
+
+// Before the analyzer is closed, the requested general statistic results are
+// printed out here. In current stage, these information are not output to
+// the files.
+// -----type
+// |__cf_id
+// |_statistics
+void TraceAnalyzer::PrintStatistics() {
+ for (int type = 0; type < kTaTypeNum; type++) {
+ if (!ta_[type].enabled) {
+ continue;
+ }
+ ta_[type].total_keys = 0;
+ ta_[type].total_access = 0;
+ ta_[type].total_succ_access = 0;
+ printf("\n################# Operation Type: %s #####################\n",
+ ta_[type].type_name.c_str());
+ if (qps_ave_.size() == kTaTypeNum + 1) {
+ printf("Peak QPS is: %u Average QPS is: %f\n", qps_peak_[type],
+ qps_ave_[type]);
+ }
+ for (auto& stat_it : ta_[type].stats) {
+ if (stat_it.second.a_count == 0) {
+ continue;
+ }
+ TraceStats& stat = stat_it.second;
+ uint64_t total_a_keys = static_cast<uint64_t>(stat.a_key_stats.size());
+ double key_size_ave = 0.0;
+ double value_size_ave = 0.0;
+ double key_size_vari = 0.0;
+ double value_size_vari = 0.0;
+ if (stat.a_count > 0) {
+ key_size_ave =
+ (static_cast<double>(stat.a_key_size_sum)) / stat.a_count;
+ value_size_ave =
+ (static_cast<double>(stat.a_value_size_sum)) / stat.a_count;
+ key_size_vari = std::sqrt((static_cast<double>(stat.a_key_size_sqsum)) /
+ stat.a_count -
+ key_size_ave * key_size_ave);
+ value_size_vari = std::sqrt(
+ (static_cast<double>(stat.a_value_size_sqsum)) / stat.a_count -
+ value_size_ave * value_size_ave);
+ }
+ if (value_size_ave == 0.0) {
+ stat.a_value_mid = 0;
+ }
+ cfs_[stat.cf_id].a_count += total_a_keys;
+ ta_[type].total_keys += total_a_keys;
+ ta_[type].total_access += stat.a_count;
+ ta_[type].total_succ_access += stat.a_succ_count;
+ printf("*********************************************************\n");
+ printf("colume family id: %u\n", stat.cf_id);
+ printf("Total number of queries to this cf by %s: %" PRIu64 "\n",
+ ta_[type].type_name.c_str(), stat.a_count);
+ printf("Total unique keys in this cf: %" PRIu64 "\n", total_a_keys);
+ printf("Average key size: %f key size medium: %" PRIu64
+ " Key size Variation: %f\n",
+ key_size_ave, stat.a_key_mid, key_size_vari);
+ if (type == kPut || type == kMerge) {
+ printf("Average value size: %f Value size medium: %" PRIu64
+ " Value size variation: %f\n",
+ value_size_ave, stat.a_value_mid, value_size_vari);
+ }
+ printf("Peak QPS is: %u Average QPS is: %f\n", stat.a_peak_qps,
+ stat.a_ave_qps);
+
+ // print the top k accessed key and its access count
+ if (FLAGS_print_top_k_access > 0) {
+ printf("The Top %d keys that are accessed:\n",
+ FLAGS_print_top_k_access);
+ while (!stat.top_k_queue.empty()) {
+ std::string hex_key = ROCKSDB_NAMESPACE::LDBCommand::StringToHex(
+ stat.top_k_queue.top().second);
+ printf("Access_count: %" PRIu64 " %s\n", stat.top_k_queue.top().first,
+ hex_key.c_str());
+ stat.top_k_queue.pop();
+ }
+ }
+
+ // print the top k access prefix range and
+ // top k prefix range with highest average access per key
+ if (FLAGS_output_prefix_cut > 0) {
+ printf("The Top %d accessed prefix range:\n", FLAGS_print_top_k_access);
+ while (!stat.top_k_prefix_access.empty()) {
+ printf("Prefix: %s Access count: %" PRIu64 "\n",
+ stat.top_k_prefix_access.top().second.c_str(),
+ stat.top_k_prefix_access.top().first);
+ stat.top_k_prefix_access.pop();
+ }
+
+ printf("The Top %d prefix with highest access per key:\n",
+ FLAGS_print_top_k_access);
+ while (!stat.top_k_prefix_ave.empty()) {
+ printf("Prefix: %s access per key: %f\n",
+ stat.top_k_prefix_ave.top().second.c_str(),
+ stat.top_k_prefix_ave.top().first);
+ stat.top_k_prefix_ave.pop();
+ }
+ }
+
+ // print the operation correlations
+ if (!FLAGS_print_correlation.empty()) {
+ for (int correlation = 0;
+ correlation <
+ static_cast<int>(analyzer_opts_.correlation_list.size());
+ correlation++) {
+ printf(
+ "The correlation statistics of '%s' after '%s' is:",
+ taIndexToOpt[analyzer_opts_.correlation_list[correlation].second]
+ .c_str(),
+ taIndexToOpt[analyzer_opts_.correlation_list[correlation].first]
+ .c_str());
+ double correlation_ave = 0.0;
+ if (stat.correlation_output[correlation].first > 0) {
+ correlation_ave =
+ (static_cast<double>(
+ stat.correlation_output[correlation].second)) /
+ (stat.correlation_output[correlation].first * 1000);
+ }
+ printf(" total numbers: %" PRIu64 " average time: %f(ms)\n",
+ stat.correlation_output[correlation].first, correlation_ave);
+ }
+ }
+ }
+ printf("*********************************************************\n");
+ printf("Total keys of '%s' is: %" PRIu64 "\n", ta_[type].type_name.c_str(),
+ ta_[type].total_keys);
+ printf("Total access is: %" PRIu64 "\n", ta_[type].total_access);
+ total_access_keys_ += ta_[type].total_keys;
+ }
+
+ // Print the overall statistic information of the trace
+ printf("\n*********************************************************\n");
+ printf("*********************************************************\n");
+ printf("The column family based statistics\n");
+ for (auto& cf : cfs_) {
+ printf("The column family id: %u\n", cf.first);
+ printf("The whole key space key numbers: %" PRIu64 "\n", cf.second.w_count);
+ printf("The accessed key space key numbers: %" PRIu64 "\n",
+ cf.second.a_count);
+ }
+
+ if (FLAGS_print_overall_stats) {
+ printf("\n*********************************************************\n");
+ printf("*********************************************************\n");
+ if (qps_peak_.size() == kTaTypeNum + 1) {
+ printf("Average QPS per second: %f Peak QPS: %u\n", qps_ave_[kTaTypeNum],
+ qps_peak_[kTaTypeNum]);
+ }
+ printf("The statistics related to query number need to times: %u\n",
+ sample_max_);
+ printf("Total_requests: %" PRIu64 " Total_accessed_keys: %" PRIu64
+ " Total_gets: %" PRIu64 " Total_write_batches: %" PRIu64
+ " Total_seeks: %" PRIu64 " Total_seek_for_prevs: %" PRIu64
+ " Total_multigets: %" PRIu64 "\n",
+ total_requests_, total_access_keys_, total_gets_, total_writes_,
+ total_seeks_, total_seek_prevs_, total_multigets_);
+ for (int type = 0; type < kTaTypeNum; type++) {
+ if (!ta_[type].enabled) {
+ continue;
+ }
+ printf("Operation: '%s' has: %" PRIu64 "\n", ta_[type].type_name.c_str(),
+ ta_[type].total_access);
+ }
+ }
+}
+
+// Write the trace sequence to file
+Status TraceAnalyzer::WriteTraceSequence(const uint32_t& type,
+ const uint32_t& cf_id,
+ const Slice& key,
+ const size_t value_size,
+ const uint64_t ts) {
+ std::string hex_key =
+ ROCKSDB_NAMESPACE::LDBCommand::StringToHex(key.ToString());
+ int ret;
+ ret = snprintf(buffer_, sizeof(buffer_), "%u %u %zu %" PRIu64 "\n", type,
+ cf_id, value_size, ts);
+ if (ret < 0) {
+ return Status::IOError("failed to format the output");
+ }
+ std::string printout(buffer_);
+ if (!FLAGS_no_key) {
+ printout = hex_key + " " + printout;
+ }
+ return trace_sequence_f_->Append(printout);
+}
+
+// The entrance function of Trace_Analyzer
+int trace_analyzer_tool(int argc, char** argv) {
+ std::string trace_path;
+ std::string output_path;
+
+ AnalyzerOptions analyzer_opts;
+
+ ParseCommandLineFlags(&argc, &argv, true);
+
+ if (!FLAGS_print_correlation.empty()) {
+ analyzer_opts.SparseCorrelationInput(FLAGS_print_correlation);
+ }
+
+ std::unique_ptr<TraceAnalyzer> analyzer(
+ new TraceAnalyzer(FLAGS_trace_path, FLAGS_output_dir, analyzer_opts));
+
+ if (!analyzer) {
+ fprintf(stderr, "Cannot initiate the trace analyzer\n");
+ exit(1);
+ }
+
+ ROCKSDB_NAMESPACE::Status s = analyzer->PrepareProcessing();
+ if (!s.ok()) {
+ fprintf(stderr, "%s\n", s.getState());
+ fprintf(stderr, "Cannot initiate the trace reader\n");
+ exit(1);
+ }
+
+ s = analyzer->StartProcessing();
+ if (!s.ok() && !FLAGS_try_process_corrupted_trace) {
+ fprintf(stderr, "%s\n", s.getState());
+ fprintf(stderr, "Cannot process the trace\n");
+ exit(1);
+ }
+
+ s = analyzer->MakeStatistics();
+ if (!s.ok()) {
+ fprintf(stderr, "%s\n", s.getState());
+ analyzer->EndProcessing();
+ fprintf(stderr, "Cannot make the statistics\n");
+ exit(1);
+ }
+
+ s = analyzer->ReProcessing();
+ if (!s.ok()) {
+ fprintf(stderr, "%s\n", s.getState());
+ fprintf(stderr, "Cannot re-process the trace for more statistics\n");
+ analyzer->EndProcessing();
+ exit(1);
+ }
+
+ s = analyzer->EndProcessing();
+ if (!s.ok()) {
+ fprintf(stderr, "%s\n", s.getState());
+ fprintf(stderr, "Cannot close the trace analyzer\n");
+ exit(1);
+ }
+
+ return 0;
+}
+
+} // namespace ROCKSDB_NAMESPACE
+
+#endif // Endif of Gflag
+#endif // RocksDB LITE
diff --git a/src/rocksdb/tools/trace_analyzer_tool.h b/src/rocksdb/tools/trace_analyzer_tool.h
new file mode 100644
index 000000000..4b885b18c
--- /dev/null
+++ b/src/rocksdb/tools/trace_analyzer_tool.h
@@ -0,0 +1,326 @@
+// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+// This source code is licensed under both the GPLv2 (found in the
+// COPYING file in the root directory) and Apache 2.0 License
+// (found in the LICENSE.Apache file in the root directory).
+
+#pragma once
+#ifndef ROCKSDB_LITE
+
+#include <list>
+#include <map>
+#include <queue>
+#include <set>
+#include <utility>
+#include <vector>
+
+#include "rocksdb/env.h"
+#include "rocksdb/trace_reader_writer.h"
+#include "rocksdb/trace_record.h"
+#include "rocksdb/write_batch.h"
+#include "trace_replay/trace_replay.h"
+
+namespace ROCKSDB_NAMESPACE {
+
+// Value sizes may be used as denominators. Replacing 0 value sizes with this
+// positive integer avoids division error.
+extern const size_t kShadowValueSize /* = 10*/;
+
+enum TraceOperationType : int {
+ kGet = 0,
+ kPut = 1,
+ kDelete = 2,
+ kSingleDelete = 3,
+ kRangeDelete = 4,
+ kMerge = 5,
+ kIteratorSeek = 6,
+ kIteratorSeekForPrev = 7,
+ kMultiGet = 8,
+ kTaTypeNum = 9
+};
+
+struct TraceUnit {
+ uint64_t ts;
+ uint32_t type;
+ uint32_t cf_id;
+ size_t value_size;
+ std::string key;
+};
+
+struct TypeCorrelation {
+ uint64_t count;
+ uint64_t total_ts;
+};
+
+struct StatsUnit {
+ uint64_t key_id;
+ uint64_t access_count;
+ uint64_t latest_ts;
+ uint64_t succ_count; // current only used to count Get if key found
+ uint32_t cf_id;
+ size_t value_size;
+ std::vector<TypeCorrelation> v_correlation;
+};
+
+class AnalyzerOptions {
+ public:
+ std::vector<std::vector<int>> correlation_map;
+ std::vector<std::pair<int, int>> correlation_list;
+
+ AnalyzerOptions();
+
+ ~AnalyzerOptions();
+
+ void SparseCorrelationInput(const std::string& in_str);
+};
+
+// Note that, for the variable names in the trace_analyzer,
+// Starting with 'a_' means the variable is used for 'accessed_keys'.
+// Starting with 'w_' means it is used for 'the whole key space'.
+// Ending with '_f' means a file write or reader pointer.
+// For example, 'a_count' means 'accessed_keys_count',
+// 'w_key_f' means 'whole_key_space_file'.
+
+struct TraceStats {
+ uint32_t cf_id;
+ std::string cf_name;
+ uint64_t a_count;
+ uint64_t a_succ_count;
+ uint64_t a_key_id;
+ uint64_t a_key_size_sqsum;
+ uint64_t a_key_size_sum;
+ uint64_t a_key_mid;
+ uint64_t a_value_size_sqsum;
+ uint64_t a_value_size_sum;
+ uint64_t a_value_mid;
+ uint32_t a_peak_qps;
+ double a_ave_qps;
+ std::map<std::string, StatsUnit> a_key_stats;
+ std::map<uint64_t, uint64_t> a_count_stats;
+ std::map<uint64_t, uint64_t> a_key_size_stats;
+ std::map<uint64_t, uint64_t> a_value_size_stats;
+ std::map<uint32_t, uint32_t> a_qps_stats;
+ std::map<uint32_t, std::map<std::string, uint32_t>> a_qps_prefix_stats;
+ std::priority_queue<std::pair<uint64_t, std::string>,
+ std::vector<std::pair<uint64_t, std::string>>,
+ std::greater<std::pair<uint64_t, std::string>>>
+ top_k_queue;
+ std::priority_queue<std::pair<uint64_t, std::string>,
+ std::vector<std::pair<uint64_t, std::string>>,
+ std::greater<std::pair<uint64_t, std::string>>>
+ top_k_prefix_access;
+ std::priority_queue<std::pair<double, std::string>,
+ std::vector<std::pair<double, std::string>>,
+ std::greater<std::pair<double, std::string>>>
+ top_k_prefix_ave;
+ std::priority_queue<std::pair<uint32_t, uint32_t>,
+ std::vector<std::pair<uint32_t, uint32_t>>,
+ std::greater<std::pair<uint32_t, uint32_t>>>
+ top_k_qps_sec;
+ std::list<TraceUnit> time_series;
+ std::vector<std::pair<uint64_t, uint64_t>> correlation_output;
+ std::map<uint32_t, uint64_t> uni_key_num;
+
+ std::unique_ptr<ROCKSDB_NAMESPACE::WritableFile> time_series_f;
+ std::unique_ptr<ROCKSDB_NAMESPACE::WritableFile> a_key_f;
+ std::unique_ptr<ROCKSDB_NAMESPACE::WritableFile> a_count_dist_f;
+ std::unique_ptr<ROCKSDB_NAMESPACE::WritableFile> a_prefix_cut_f;
+ std::unique_ptr<ROCKSDB_NAMESPACE::WritableFile> a_value_size_f;
+ std::unique_ptr<ROCKSDB_NAMESPACE::WritableFile> a_key_size_f;
+ std::unique_ptr<ROCKSDB_NAMESPACE::WritableFile> a_key_num_f;
+ std::unique_ptr<ROCKSDB_NAMESPACE::WritableFile> a_qps_f;
+ std::unique_ptr<ROCKSDB_NAMESPACE::WritableFile> a_top_qps_prefix_f;
+ std::unique_ptr<ROCKSDB_NAMESPACE::WritableFile> w_key_f;
+ std::unique_ptr<ROCKSDB_NAMESPACE::WritableFile> w_prefix_cut_f;
+
+ TraceStats();
+ ~TraceStats();
+ TraceStats(const TraceStats&) = delete;
+ TraceStats& operator=(const TraceStats&) = delete;
+ TraceStats(TraceStats&&) = default;
+ TraceStats& operator=(TraceStats&&) = default;
+};
+
+struct TypeUnit {
+ std::string type_name;
+ bool enabled;
+ uint64_t total_keys;
+ uint64_t total_access;
+ uint64_t total_succ_access;
+ uint32_t sample_count;
+ std::map<uint32_t, TraceStats> stats;
+ TypeUnit() = default;
+ ~TypeUnit() = default;
+ TypeUnit(const TypeUnit&) = delete;
+ TypeUnit& operator=(const TypeUnit&) = delete;
+ TypeUnit(TypeUnit&&) = default;
+ TypeUnit& operator=(TypeUnit&&) = default;
+};
+
+struct CfUnit {
+ uint32_t cf_id;
+ uint64_t w_count; // total keys in this cf if we use the whole key space
+ uint64_t a_count; // the total keys in this cf that are accessed
+ std::map<uint64_t, uint64_t> w_key_size_stats; // whole key space key size
+ // statistic this cf
+ std::map<uint32_t, uint32_t> cf_qps;
+};
+
+class TraceAnalyzer : private TraceRecord::Handler,
+ private WriteBatch::Handler {
+ public:
+ TraceAnalyzer(std::string& trace_path, std::string& output_path,
+ AnalyzerOptions _analyzer_opts);
+ ~TraceAnalyzer();
+
+ Status PrepareProcessing();
+
+ Status StartProcessing();
+
+ Status MakeStatistics();
+
+ Status ReProcessing();
+
+ Status EndProcessing();
+
+ Status WriteTraceUnit(TraceUnit& unit);
+
+ std::vector<TypeUnit>& GetTaVector() { return ta_; }
+
+ private:
+ using TraceRecord::Handler::Handle;
+ Status Handle(const WriteQueryTraceRecord& record,
+ std::unique_ptr<TraceRecordResult>* result) override;
+ Status Handle(const GetQueryTraceRecord& record,
+ std::unique_ptr<TraceRecordResult>* result) override;
+ Status Handle(const IteratorSeekQueryTraceRecord& record,
+ std::unique_ptr<TraceRecordResult>* result) override;
+ Status Handle(const MultiGetQueryTraceRecord& record,
+ std::unique_ptr<TraceRecordResult>* result) override;
+
+ using WriteBatch::Handler::PutCF;
+ Status PutCF(uint32_t column_family_id, const Slice& key,
+ const Slice& value) override;
+
+ using WriteBatch::Handler::DeleteCF;
+ Status DeleteCF(uint32_t column_family_id, const Slice& key) override;
+
+ using WriteBatch::Handler::SingleDeleteCF;
+ Status SingleDeleteCF(uint32_t column_family_id, const Slice& key) override;
+
+ using WriteBatch::Handler::DeleteRangeCF;
+ Status DeleteRangeCF(uint32_t column_family_id, const Slice& begin_key,
+ const Slice& end_key) override;
+
+ using WriteBatch::Handler::MergeCF;
+ Status MergeCF(uint32_t column_family_id, const Slice& key,
+ const Slice& value) override;
+
+ // The following hanlders are not implemented, return Status::OK() to avoid
+ // the running time assertion and other irrelevant falures.
+ using WriteBatch::Handler::PutBlobIndexCF;
+ Status PutBlobIndexCF(uint32_t /*column_family_id*/, const Slice& /*key*/,
+ const Slice& /*value*/) override {
+ return Status::OK();
+ }
+
+ // The default implementation of LogData does nothing.
+ using WriteBatch::Handler::LogData;
+ void LogData(const Slice& /*blob*/) override {}
+
+ using WriteBatch::Handler::MarkBeginPrepare;
+ Status MarkBeginPrepare(bool = false) override { return Status::OK(); }
+
+ using WriteBatch::Handler::MarkEndPrepare;
+ Status MarkEndPrepare(const Slice& /*xid*/) override { return Status::OK(); }
+
+ using WriteBatch::Handler::MarkNoop;
+ Status MarkNoop(bool /*empty_batch*/) override { return Status::OK(); }
+
+ using WriteBatch::Handler::MarkRollback;
+ Status MarkRollback(const Slice& /*xid*/) override { return Status::OK(); }
+
+ using WriteBatch::Handler::MarkCommit;
+ Status MarkCommit(const Slice& /*xid*/) override { return Status::OK(); }
+
+ using WriteBatch::Handler::MarkCommitWithTimestamp;
+ Status MarkCommitWithTimestamp(const Slice& /*xid*/,
+ const Slice& /*commit_ts*/) override {
+ return Status::OK();
+ }
+
+ // Process each trace operation and output the analysis result to
+ // stdout/files.
+ Status OutputAnalysisResult(TraceOperationType op_type, uint64_t timestamp,
+ std::vector<uint32_t> cf_ids,
+ std::vector<Slice> keys,
+ std::vector<size_t> value_sizes);
+
+ Status OutputAnalysisResult(TraceOperationType op_type, uint64_t timestamp,
+ uint32_t cf_id, const Slice& key,
+ size_t value_size);
+
+ ROCKSDB_NAMESPACE::Env* env_;
+ EnvOptions env_options_;
+ std::unique_ptr<TraceReader> trace_reader_;
+ size_t offset_;
+ char buffer_[1024];
+ // Timestamp of a WriteBatch, used in its iteration.
+ uint64_t write_batch_ts_;
+ std::string trace_name_;
+ std::string output_path_;
+ AnalyzerOptions analyzer_opts_;
+ uint64_t total_requests_;
+ uint64_t total_access_keys_;
+ uint64_t total_gets_;
+ uint64_t total_writes_;
+ uint64_t total_seeks_;
+ uint64_t total_seek_prevs_;
+ uint64_t total_multigets_;
+ uint64_t trace_create_time_;
+ uint64_t begin_time_;
+ uint64_t end_time_;
+ uint64_t time_series_start_;
+ uint32_t sample_max_;
+ uint32_t cur_time_sec_;
+ std::unique_ptr<ROCKSDB_NAMESPACE::WritableFile>
+ trace_sequence_f_; // readable trace
+ std::unique_ptr<ROCKSDB_NAMESPACE::WritableFile> qps_f_; // overall qps
+ std::unique_ptr<ROCKSDB_NAMESPACE::WritableFile>
+ cf_qps_f_; // The qps of each CF>
+ std::vector<TypeUnit> ta_; // The main statistic collecting data structure
+ std::map<uint32_t, CfUnit> cfs_; // All the cf_id appears in this trace;
+ std::vector<uint32_t> qps_peak_;
+ std::vector<double> qps_ave_;
+
+ Status ReadTraceHeader(Trace* header);
+ Status ReadTraceFooter(Trace* footer);
+ Status ReadTraceRecord(Trace* trace);
+ Status KeyStatsInsertion(const uint32_t& type, const uint32_t& cf_id,
+ const std::string& key, const size_t value_size,
+ const uint64_t ts);
+ Status StatsUnitCorrelationUpdate(StatsUnit& unit, const uint32_t& type,
+ const uint64_t& ts, const std::string& key);
+ Status OpenStatsOutputFiles(const std::string& type, TraceStats& new_stats);
+ Status CreateOutputFile(
+ const std::string& type, const std::string& cf_name,
+ const std::string& ending,
+ std::unique_ptr<ROCKSDB_NAMESPACE::WritableFile>* f_ptr);
+ Status CloseOutputFiles();
+
+ void PrintStatistics();
+ Status TraceUnitWriter(
+ std::unique_ptr<ROCKSDB_NAMESPACE::WritableFile>& f_ptr, TraceUnit& unit);
+ Status WriteTraceSequence(const uint32_t& type, const uint32_t& cf_id,
+ const Slice& key, const size_t value_size,
+ const uint64_t ts);
+ Status MakeStatisticKeyStatsOrPrefix(TraceStats& stats);
+ Status MakeStatisticCorrelation(TraceStats& stats, StatsUnit& unit);
+ Status MakeStatisticQPS();
+ int db_version_;
+};
+
+int trace_analyzer_tool(int argc, char** argv);
+
+} // namespace ROCKSDB_NAMESPACE
+
+#endif // ROCKSDB_LITE
diff --git a/src/rocksdb/tools/verify_random_db.sh b/src/rocksdb/tools/verify_random_db.sh
new file mode 100755
index 000000000..fbe5b75fd
--- /dev/null
+++ b/src/rocksdb/tools/verify_random_db.sh
@@ -0,0 +1,41 @@
+#!/usr/bin/env bash
+# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
+#
+# A shell script to verify DB generated by generate_random_db.sh cannot opened and read correct data.
+# ./ldb needs to be avaible to be executed.
+#
+# Usage: <SCRIPT> <DB Path>
+
+scriptpath=`dirname $BASH_SOURCE`
+if [ "$#" -lt 2 ]; then
+ echo "usage: $BASH_SOURCE <db_directory> <compare_base_db_directory> [dump_file_name] [if_try_load_options] [if_ignore_unknown_options]"
+ exit 1
+fi
+
+db_dir=$1
+base_db_dir=$2
+dump_file_name=${3:-"dump_file.txt"}
+try_load_options=${4:-"1"}
+ignore_unknown_options=${5:-"0"}
+db_dump=$db_dir"/"$dump_file_name
+base_db_dump=$base_db_dir"/"$dump_file_name
+extra_params=
+
+if [ "$try_load_options" = "0" ]; then
+ extra_params=" --try_load_options=false"
+elif [ "$try_load_options" = "1" ]; then
+ extra_params=" --try_load_options=true"
+fi
+
+if [ "$ignore_unknown_options" = "1" ]; then
+ extra_params="$extra_params --ignore_unknown_options"
+fi
+
+set -e
+echo == Dumping data from $db_dir to $db_dump
+./ldb dump --db=$db_dir $extra_params > $db_dump
+
+echo == Dumping data from $base_db_dir to $base_db_dump
+./ldb dump --db=$base_db_dir $extra_params > $base_db_dump
+
+diff $db_dump $base_db_dump
diff --git a/src/rocksdb/tools/write_external_sst.sh b/src/rocksdb/tools/write_external_sst.sh
new file mode 100755
index 000000000..be01ae022
--- /dev/null
+++ b/src/rocksdb/tools/write_external_sst.sh
@@ -0,0 +1,26 @@
+#!/usr/bin/env bash
+# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
+#
+#
+
+if [ "$#" -lt 3 ]; then
+ echo "usage: $BASH_SOURCE <input_data_path> <DB Path> <extern SST dir>"
+ exit 1
+fi
+
+input_data_dir=$1
+db_dir=$2
+extern_sst_dir=$3
+rm -rf $db_dir
+mkdir -p $extern_sst_dir
+
+set -e
+
+n=0
+
+for f in `find $input_data_dir -name sorted_data*`
+do
+ echo == Writing external SST file $f to $extern_sst_dir/extern_sst${n}
+ ./ldb --db=$db_dir --create_if_missing write_extern_sst $extern_sst_dir/extern_sst${n} < $f
+ let "n = n + 1"
+done
diff --git a/src/rocksdb/tools/write_stress.cc b/src/rocksdb/tools/write_stress.cc
new file mode 100644
index 000000000..ba5bd3f4f
--- /dev/null
+++ b/src/rocksdb/tools/write_stress.cc
@@ -0,0 +1,309 @@
+// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
+// This source code is licensed under both the GPLv2 (found in the
+// COPYING file in the root directory) and Apache 2.0 License
+// (found in the LICENSE.Apache file in the root directory).
+//
+//
+// The goal of this tool is to be a simple stress test with focus on catching:
+// * bugs in compaction/flush processes, especially the ones that cause
+// assertion errors
+// * bugs in the code that deletes obsolete files
+//
+// There are two parts of the test:
+// * write_stress, a binary that writes to the database
+// * write_stress_runner.py, a script that invokes and kills write_stress
+//
+// Here are some interesting parts of write_stress:
+// * Runs with very high concurrency of compactions and flushes (32 threads
+// total) and tries to create a huge amount of small files
+// * The keys written to the database are not uniformly distributed -- there is
+// a 3-character prefix that mutates occasionally (in prefix mutator thread), in
+// such a way that the first character mutates slower than second, which mutates
+// slower than third character. That way, the compaction stress tests some
+// interesting compaction features like trivial moves and bottommost level
+// calculation
+// * There is a thread that creates an iterator, holds it for couple of seconds
+// and then iterates over all keys. This is supposed to test RocksDB's abilities
+// to keep the files alive when there are references to them.
+// * Some writes trigger WAL sync. This is stress testing our WAL sync code.
+// * At the end of the run, we make sure that we didn't leak any of the sst
+// files
+//
+// write_stress_runner.py changes the mode in which we run write_stress and also
+// kills and restarts it. There are some interesting characteristics:
+// * At the beginning we divide the full test runtime into smaller parts --
+// shorter runtimes (couple of seconds) and longer runtimes (100, 1000) seconds
+// * The first time we run write_stress, we destroy the old DB. Every next time
+// during the test, we use the same DB.
+// * We can run in kill mode or clean-restart mode. Kill mode kills the
+// write_stress violently.
+// * We can run in mode where delete_obsolete_files_with_fullscan is true or
+// false
+// * We can run with low_open_files mode turned on or off. When it's turned on,
+// we configure table cache to only hold a couple of files -- that way we need
+// to reopen files every time we access them.
+//
+// Another goal was to create a stress test without a lot of parameters. So
+// tools/write_stress_runner.py should only take one parameter -- runtime_sec
+// and it should figure out everything else on its own.
+
+#include <cstdio>
+
+#ifndef GFLAGS
+int main() {
+ fprintf(stderr, "Please install gflags to run rocksdb tools\n");
+ return 1;
+}
+#else
+
+#include <atomic>
+#include <cinttypes>
+#include <random>
+#include <set>
+#include <string>
+#include <thread>
+
+#include "file/filename.h"
+#include "port/port.h"
+#include "rocksdb/db.h"
+#include "rocksdb/env.h"
+#include "rocksdb/options.h"
+#include "rocksdb/slice.h"
+#include "rocksdb/system_clock.h"
+#include "util/gflags_compat.h"
+
+using GFLAGS_NAMESPACE::ParseCommandLineFlags;
+using GFLAGS_NAMESPACE::RegisterFlagValidator;
+using GFLAGS_NAMESPACE::SetUsageMessage;
+
+DEFINE_int32(key_size, 10, "Key size");
+DEFINE_int32(value_size, 100, "Value size");
+DEFINE_string(db, "", "Use the db with the following name.");
+DEFINE_bool(destroy_db, true,
+ "Destroy the existing DB before running the test");
+
+DEFINE_int32(runtime_sec, 10 * 60, "How long are we running for, in seconds");
+DEFINE_int32(seed, 139, "Random seed");
+
+DEFINE_double(prefix_mutate_period_sec, 1.0,
+ "How often are we going to mutate the prefix");
+DEFINE_double(first_char_mutate_probability, 0.1,
+ "How likely are we to mutate the first char every period");
+DEFINE_double(second_char_mutate_probability, 0.2,
+ "How likely are we to mutate the second char every period");
+DEFINE_double(third_char_mutate_probability, 0.5,
+ "How likely are we to mutate the third char every period");
+
+DEFINE_int32(iterator_hold_sec, 5,
+ "How long will the iterator hold files before it gets destroyed");
+
+DEFINE_double(sync_probability, 0.01, "How often are we syncing writes");
+DEFINE_bool(delete_obsolete_files_with_fullscan, false,
+ "If true, we delete obsolete files after each compaction/flush "
+ "using GetChildren() API");
+DEFINE_bool(low_open_files_mode, false,
+ "If true, we set max_open_files to 20, so that every file access "
+ "needs to reopen it");
+
+namespace ROCKSDB_NAMESPACE {
+
+static const int kPrefixSize = 3;
+
+class WriteStress {
+ public:
+ WriteStress() : stop_(false) {
+ // initialize key_prefix
+ for (int i = 0; i < kPrefixSize; ++i) {
+ key_prefix_[i].store('a');
+ }
+
+ // Choose a location for the test database if none given with --db=<path>
+ if (FLAGS_db.empty()) {
+ std::string default_db_path;
+ Env::Default()->GetTestDirectory(&default_db_path);
+ default_db_path += "/write_stress";
+ FLAGS_db = default_db_path;
+ }
+
+ Options options;
+ if (FLAGS_destroy_db) {
+ DestroyDB(FLAGS_db, options); // ignore
+ }
+
+ // make the LSM tree deep, so that we have many concurrent flushes and
+ // compactions
+ options.create_if_missing = true;
+ options.write_buffer_size = 256 * 1024; // 256k
+ options.max_bytes_for_level_base = 1 * 1024 * 1024; // 1MB
+ options.target_file_size_base = 100 * 1024; // 100k
+ options.max_write_buffer_number = 16;
+ options.max_background_compactions = 16;
+ options.max_background_flushes = 16;
+ options.max_open_files = FLAGS_low_open_files_mode ? 20 : -1;
+ if (FLAGS_delete_obsolete_files_with_fullscan) {
+ options.delete_obsolete_files_period_micros = 0;
+ }
+
+ // open DB
+ DB* db;
+ Status s = DB::Open(options, FLAGS_db, &db);
+ if (!s.ok()) {
+ fprintf(stderr, "Can't open database: %s\n", s.ToString().c_str());
+ std::abort();
+ }
+ db_.reset(db);
+ }
+
+ void WriteThread() {
+ std::mt19937 rng(static_cast<unsigned int>(FLAGS_seed));
+ std::uniform_real_distribution<double> dist(0, 1);
+
+ auto random_string = [](std::mt19937& r, int len) {
+ std::uniform_int_distribution<int> char_dist('a', 'z');
+ std::string ret;
+ for (int i = 0; i < len; ++i) {
+ ret += static_cast<char>(char_dist(r));
+ }
+ return ret;
+ };
+
+ while (!stop_.load(std::memory_order_relaxed)) {
+ std::string prefix;
+ prefix.resize(kPrefixSize);
+ for (int i = 0; i < kPrefixSize; ++i) {
+ prefix[i] = key_prefix_[i].load(std::memory_order_relaxed);
+ }
+ auto key = prefix + random_string(rng, FLAGS_key_size - kPrefixSize);
+ auto value = random_string(rng, FLAGS_value_size);
+ WriteOptions woptions;
+ woptions.sync = dist(rng) < FLAGS_sync_probability;
+ auto s = db_->Put(woptions, key, value);
+ if (!s.ok()) {
+ fprintf(stderr, "Write to DB failed: %s\n", s.ToString().c_str());
+ std::abort();
+ }
+ }
+ }
+
+ void IteratorHoldThread() {
+ while (!stop_.load(std::memory_order_relaxed)) {
+ std::unique_ptr<Iterator> iterator(db_->NewIterator(ReadOptions()));
+ SystemClock::Default()->SleepForMicroseconds(FLAGS_iterator_hold_sec *
+ 1000 * 1000LL);
+ for (iterator->SeekToFirst(); iterator->Valid(); iterator->Next()) {
+ }
+ if (!iterator->status().ok()) {
+ fprintf(stderr, "Iterator statuts not OK: %s\n",
+ iterator->status().ToString().c_str());
+ std::abort();
+ }
+ }
+ }
+
+ void PrefixMutatorThread() {
+ std::mt19937 rng(static_cast<unsigned int>(FLAGS_seed));
+ std::uniform_real_distribution<double> dist(0, 1);
+ std::uniform_int_distribution<int> char_dist('a', 'z');
+ while (!stop_.load(std::memory_order_relaxed)) {
+ SystemClock::Default()->SleepForMicroseconds(
+ static_cast<int>(FLAGS_prefix_mutate_period_sec * 1000 * 1000LL));
+ if (dist(rng) < FLAGS_first_char_mutate_probability) {
+ key_prefix_[0].store(static_cast<char>(char_dist(rng)),
+ std::memory_order_relaxed);
+ }
+ if (dist(rng) < FLAGS_second_char_mutate_probability) {
+ key_prefix_[1].store(static_cast<char>(char_dist(rng)),
+ std::memory_order_relaxed);
+ }
+ if (dist(rng) < FLAGS_third_char_mutate_probability) {
+ key_prefix_[2].store(static_cast<char>(char_dist(rng)),
+ std::memory_order_relaxed);
+ }
+ }
+ }
+
+ int Run() {
+ threads_.emplace_back([&]() { WriteThread(); });
+ threads_.emplace_back([&]() { PrefixMutatorThread(); });
+ threads_.emplace_back([&]() { IteratorHoldThread(); });
+
+ if (FLAGS_runtime_sec == -1) {
+ // infinite runtime, until we get killed
+ while (true) {
+ SystemClock::Default()->SleepForMicroseconds(1000 * 1000);
+ }
+ }
+
+ SystemClock::Default()->SleepForMicroseconds(FLAGS_runtime_sec * 1000 *
+ 1000);
+
+ stop_.store(true, std::memory_order_relaxed);
+ for (auto& t : threads_) {
+ t.join();
+ }
+ threads_.clear();
+
+// Skip checking for leaked files in ROCKSDB_LITE since we don't have access to
+// function GetLiveFilesMetaData
+#ifndef ROCKSDB_LITE
+ // let's see if we leaked some files
+ db_->PauseBackgroundWork();
+ std::vector<LiveFileMetaData> metadata;
+ db_->GetLiveFilesMetaData(&metadata);
+ std::set<uint64_t> sst_file_numbers;
+ for (const auto& file : metadata) {
+ uint64_t number;
+ FileType type;
+ if (!ParseFileName(file.name, &number, "LOG", &type)) {
+ continue;
+ }
+ if (type == kTableFile) {
+ sst_file_numbers.insert(number);
+ }
+ }
+
+ std::vector<std::string> children;
+ Env::Default()->GetChildren(FLAGS_db, &children);
+ for (const auto& child : children) {
+ uint64_t number;
+ FileType type;
+ if (!ParseFileName(child, &number, "LOG", &type)) {
+ continue;
+ }
+ if (type == kTableFile) {
+ if (sst_file_numbers.find(number) == sst_file_numbers.end()) {
+ fprintf(stderr,
+ "Found a table file in DB path that should have been "
+ "deleted: %s\n",
+ child.c_str());
+ std::abort();
+ }
+ }
+ }
+ db_->ContinueBackgroundWork();
+#endif // !ROCKSDB_LITE
+
+ return 0;
+ }
+
+ private:
+ // each key is prepended with this prefix. we occasionally change it. third
+ // letter is changed more frequently than second, which is changed more
+ // frequently than the first one.
+ std::atomic<char> key_prefix_[kPrefixSize];
+ std::atomic<bool> stop_;
+ std::vector<port::Thread> threads_;
+ std::unique_ptr<DB> db_;
+};
+
+} // namespace ROCKSDB_NAMESPACE
+
+int main(int argc, char** argv) {
+ SetUsageMessage(std::string("\nUSAGE:\n") + std::string(argv[0]) +
+ " [OPTIONS]...");
+ ParseCommandLineFlags(&argc, &argv, true);
+ ROCKSDB_NAMESPACE::WriteStress write_stress;
+ return write_stress.Run();
+}
+
+#endif // GFLAGS
diff --git a/src/rocksdb/tools/write_stress_runner.py b/src/rocksdb/tools/write_stress_runner.py
new file mode 100644
index 000000000..f39f79cd4
--- /dev/null
+++ b/src/rocksdb/tools/write_stress_runner.py
@@ -0,0 +1,83 @@
+#!/usr/bin/env python3
+# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
+from __future__ import absolute_import, division, print_function, unicode_literals
+
+import argparse
+import random
+
+import subprocess
+import sys
+import time
+
+
+def generate_runtimes(total_runtime):
+ # combination of short runtimes and long runtimes, with heavier
+ # weight on short runtimes
+ possible_runtimes_sec = list(range(1, 10)) + list(range(1, 20)) + [100, 1000]
+ runtimes = []
+ while total_runtime > 0:
+ chosen = random.choice(possible_runtimes_sec)
+ chosen = min(chosen, total_runtime)
+ runtimes.append(chosen)
+ total_runtime -= chosen
+ return runtimes
+
+
+def main(args):
+ runtimes = generate_runtimes(int(args.runtime_sec))
+ print(
+ "Going to execute write stress for " + str(runtimes)
+ ) # noqa: E999 T25377293 Grandfathered in
+ first_time = True
+
+ for runtime in runtimes:
+ kill = random.choice([False, True])
+
+ cmd = "./write_stress --runtime_sec=" + ("-1" if kill else str(runtime))
+
+ if len(args.db) > 0:
+ cmd = cmd + " --db=" + args.db
+
+ if first_time:
+ first_time = False
+ else:
+ # use current db
+ cmd = cmd + " --destroy_db=false"
+ if random.choice([False, True]):
+ cmd = cmd + " --delete_obsolete_files_with_fullscan=true"
+ if random.choice([False, True]):
+ cmd = cmd + " --low_open_files_mode=true"
+
+ print(
+ "Running write_stress for %d seconds (%s): %s"
+ % (runtime, ("kill-mode" if kill else "clean-shutdown-mode"), cmd)
+ )
+
+ child = subprocess.Popen([cmd], shell=True)
+ killtime = time.time() + runtime
+ while not kill or time.time() < killtime:
+ time.sleep(1)
+ if child.poll() is not None:
+ if child.returncode == 0:
+ break
+ else:
+ print(
+ "ERROR: write_stress died with exitcode=%d\n" % child.returncode
+ )
+ sys.exit(1)
+ if kill:
+ child.kill()
+ # breathe
+ time.sleep(3)
+
+
+if __name__ == "__main__":
+ random.seed(time.time())
+ parser = argparse.ArgumentParser(
+ description="This script runs and kills \
+ write_stress multiple times"
+ )
+ parser.add_argument("--runtime_sec", default="1000")
+ parser.add_argument("--db", default="")
+ args = parser.parse_args()
+ main(args)