diff --git a/.github/README.md b/.github/README.md
new file mode 120000
index 000000000000..e5c578ba74b5
--- /dev/null
+++ b/.github/README.md
@@ -0,0 +1 @@
+../doc/benchcoin.md
\ No newline at end of file
diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml
new file mode 100644
index 000000000000..f58da7c5bce5
--- /dev/null
+++ b/.github/workflows/benchmark.yml
@@ -0,0 +1,186 @@
+name: Benchmark
+on:
+ pull_request:
+ branches:
+ - master
+
+jobs:
+ build-binaries:
+ runs-on: [self-hosted, linux, x64]
+ env:
+ BASE_SHA: ${{ github.event.pull_request.base.sha }}
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 1
+
+ - name: Fetch base commit
+ run: |
+ echo "HEAD_SHA=$(git rev-parse HEAD)" >> "$GITHUB_ENV"
+ git fetch --depth=1 origin ${{ github.event.pull_request.base.sha }}
+
+ - name: Build both binaries
+ run: |
+ nix develop --command python3 bench.py build \
+ -o ${{ runner.temp }}/binaries \
+ $BASE_SHA:base $HEAD_SHA:head
+
+ - name: Upload binaries
+ uses: actions/upload-artifact@v4
+ with:
+ name: bitcoind-binaries
+ path: ${{ runner.temp }}/binaries/
+
+ uninstrumented:
+ needs: build-binaries
+ strategy:
+ matrix:
+ include:
+ - name: mainnet-default-uninstrumented
+ timeout: 600
+ dbcache: 450
+ - name: mainnet-large-uninstrumented
+ timeout: 600
+ dbcache: 32000
+ runs-on: [self-hosted, linux, x64]
+ timeout-minutes: ${{ matrix.timeout }}
+ env:
+ ORIGINAL_DATADIR: /data/pruned-840k
+ BASE_SHA: ${{ github.event.pull_request.base.sha }}
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 1
+
+ - name: Download binaries
+ uses: actions/download-artifact@v4
+ with:
+ name: bitcoind-binaries
+ path: ${{ runner.temp }}/binaries
+
+ - name: Set binary permissions
+ run: |
+ chmod +x ${{ runner.temp }}/binaries/base/bitcoind
+ chmod +x ${{ runner.temp }}/binaries/head/bitcoind
+
+ - name: Fetch base commit
+ run: |
+ echo "HEAD_SHA=$(git rev-parse HEAD)" >> "$GITHUB_ENV"
+ git fetch --depth=1 origin ${{ github.event.pull_request.base.sha }}
+
+ - name: Run benchmark
+ run: |
+ nix develop --command python3 bench.py --profile ci run \
+ --datadir $ORIGINAL_DATADIR \
+ --tmp-datadir ${{ runner.temp }}/datadir \
+ --output-dir ${{ runner.temp }}/output \
+ --dbcache ${{ matrix.dbcache }} \
+ base:${{ runner.temp }}/binaries/base/bitcoind \
+ head:${{ runner.temp }}/binaries/head/bitcoind
+
+ - name: Upload results
+ uses: actions/upload-artifact@v4
+ with:
+ name: result-${{ matrix.name }}
+ path: ${{ runner.temp }}/output/results.json
+
+ - name: Write context metadata
+ env:
+ GITHUB_CONTEXT: ${{ toJSON(github) }}
+ RUNNER_CONTEXT: ${{ toJSON(runner) }}
+ run: |
+ mkdir -p ${{ runner.temp }}/contexts
+ echo "$GITHUB_CONTEXT" | nix develop --command jq "del(.token)" > ${{ runner.temp }}/contexts/github.json
+ echo "$RUNNER_CONTEXT" > ${{ runner.temp }}/contexts/runner.json
+
+ - name: Upload context metadata
+ uses: actions/upload-artifact@v4
+ with:
+ name: run-metadata-${{ matrix.name }}
+ path: ${{ runner.temp }}/contexts/
+
+ instrumented:
+ needs: build-binaries
+ strategy:
+ matrix:
+ include:
+ - name: mainnet-default-instrumented
+ timeout: 600
+ dbcache: 450
+ - name: mainnet-large-instrumented
+ timeout: 600
+ dbcache: 32000
+ runs-on: [self-hosted, linux, x64]
+ timeout-minutes: ${{ matrix.timeout }}
+ env:
+ ORIGINAL_DATADIR: /data/pruned-840k
+ BASE_SHA: ${{ github.event.pull_request.base.sha }}
+ steps:
+ - name: Checkout repo
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 1
+
+ - name: Download binaries
+ uses: actions/download-artifact@v4
+ with:
+ name: bitcoind-binaries
+ path: ${{ runner.temp }}/binaries
+
+ - name: Set binary permissions
+ run: |
+ chmod +x ${{ runner.temp }}/binaries/base/bitcoind
+ chmod +x ${{ runner.temp }}/binaries/head/bitcoind
+
+ - name: Fetch base commit
+ run: |
+ echo "HEAD_SHA=$(git rev-parse HEAD)" >> "$GITHUB_ENV"
+ git fetch --depth=1 origin ${{ github.event.pull_request.base.sha }}
+
+ - name: Run instrumented benchmark
+ run: |
+ nix develop --command python3 bench.py --profile ci run \
+ --instrumented \
+ --datadir $ORIGINAL_DATADIR \
+ --tmp-datadir ${{ runner.temp }}/datadir \
+ --output-dir ${{ runner.temp }}/output \
+ --dbcache ${{ matrix.dbcache }} \
+ base:${{ runner.temp }}/binaries/base/bitcoind \
+ head:${{ runner.temp }}/binaries/head/bitcoind
+
+ - name: Upload results
+ uses: actions/upload-artifact@v4
+ with:
+ name: result-${{ matrix.name }}
+ path: ${{ runner.temp }}/output/results.json
+
+ - name: Upload plots
+ uses: actions/upload-artifact@v4
+ with:
+ name: pngs-${{ matrix.name }}
+ path: ${{ runner.temp }}/output/plots/*.png
+ if-no-files-found: ignore
+
+ - name: Upload flamegraphs
+ uses: actions/upload-artifact@v4
+ with:
+ name: flamegraph-${{ matrix.name }}
+ path: ${{ runner.temp }}/output/*-flamegraph.svg
+ if-no-files-found: ignore
+
+ - name: Write context metadata
+ env:
+ GITHUB_CONTEXT: ${{ toJSON(github) }}
+ RUNNER_CONTEXT: ${{ toJSON(runner) }}
+ run: |
+ mkdir -p ${{ runner.temp }}/contexts
+ echo "$GITHUB_CONTEXT" | nix develop --command jq "del(.token)" > ${{ runner.temp }}/contexts/github.json
+ echo "$RUNNER_CONTEXT" > ${{ runner.temp }}/contexts/runner.json
+
+ - name: Upload context metadata
+ uses: actions/upload-artifact@v4
+ with:
+ name: run-metadata-${{ matrix.name }}
+ path: ${{ runner.temp }}/contexts/
diff --git a/.github/workflows/publish-results.yml b/.github/workflows/publish-results.yml
new file mode 100644
index 000000000000..72b1bdd39527
--- /dev/null
+++ b/.github/workflows/publish-results.yml
@@ -0,0 +1,143 @@
+name: Publish Results
+on:
+ workflow_run:
+ workflows: ["Benchmark"]
+ types: [completed]
+jobs:
+ build:
+ runs-on: ubuntu-latest
+ if: ${{ github.event.workflow_run.conclusion == 'success' }}
+ permissions:
+ actions: read
+ contents: write
+ checks: read
+ env:
+ NETWORKS: "mainnet-default-instrumented,mainnet-large-instrumented,mainnet-default-uninstrumented,mainnet-large-uninstrumented"
+ outputs:
+ speedups: ${{ steps.generate.outputs.speedups }}
+ pr-number: ${{ steps.metadata.outputs.pr-number }}
+ result-url: ${{ steps.generate.outputs.result-url }}
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ ref: gh-pages
+
+ - name: Checkout benchcoin tools
+ uses: actions/checkout@v4
+ with:
+ ref: master
+ path: benchcoin-tools
+
+ - name: Download artifacts
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ gh run download ${{ github.event.workflow_run.id }} --repo ${{ github.repository }}
+
+ - name: Extract artifacts
+ run: |
+ for network in ${NETWORKS//,/ }; do
+ # Create network-specific directories with results
+ if [ -d "result-${network}" ]; then
+ mkdir -p "${network}-results"
+ mv "result-${network}/results.json" "${network}-results/"
+ fi
+
+ # Copy flamegraphs into network results directory
+ if [ -d "flamegraph-${network}" ]; then
+ cp -r "flamegraph-${network}"/* "${network}-results/" 2>/dev/null || true
+ fi
+
+ # Copy plots into network results directory
+ if [ -d "pngs-${network}" ]; then
+ mkdir -p "${network}-results/plots"
+ cp -r "pngs-${network}"/* "${network}-results/plots/" 2>/dev/null || true
+ fi
+
+ # Keep metadata separate for extraction
+ if [ -d "run-metadata-${network}" ]; then
+ mkdir -p "${network}-metadata"
+ mv "run-metadata-${network}"/* "${network}-metadata/"
+ fi
+ done
+
+ - name: Extract metadata
+ id: metadata
+ run: |
+ # Find PR number and run ID from any available metadata
+ for network in ${NETWORKS//,/ }; do
+ if [ -f "${network}-metadata/github.json" ]; then
+ PR_NUMBER=$(jq -r '.event.pull_request.number // "main"' "${network}-metadata/github.json")
+ RUN_ID=$(jq -r '.run_id' "${network}-metadata/github.json")
+ echo "pr-number=${PR_NUMBER}" >> $GITHUB_OUTPUT
+ echo "run-id=${RUN_ID}" >> $GITHUB_OUTPUT
+ echo "Found metadata: PR=${PR_NUMBER}, Run=${RUN_ID}"
+ break
+ fi
+ done
+
+ - name: Setup Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.12'
+
+ - name: Generate report
+ id: generate
+ env:
+ PR_NUMBER: ${{ steps.metadata.outputs.pr-number }}
+ RUN_ID: ${{ steps.metadata.outputs.run-id }}
+ run: |
+ cd benchcoin-tools
+
+ # Build network arguments
+ NETWORK_ARGS=""
+ for network in ${NETWORKS//,/ }; do
+ if [ -d "../${network}-results" ]; then
+ NETWORK_ARGS="${NETWORK_ARGS} --network ${network}:../${network}-results"
+ fi
+ done
+
+ # Generate report
+ python3 bench.py report \
+ ${NETWORK_ARGS} \
+ --pr-number "${PR_NUMBER}" \
+ --run-id "${RUN_ID}" \
+ --update-index \
+ "../results/pr-${PR_NUMBER}/${RUN_ID}"
+
+ # Read speedups from generated results.json
+ SPEEDUPS=$(jq -r '.speedups | to_entries | map(select(.key | contains("uninstrumented"))) | map("\(.key): \(.value)%") | join(", ")' "../results/pr-${PR_NUMBER}/${RUN_ID}/results.json")
+ echo "speedups=${SPEEDUPS}" >> $GITHUB_OUTPUT
+
+ RESULT_URL="https://${{ github.repository_owner }}.github.io/${{ github.event.repository.name }}/results/pr-${PR_NUMBER}/${RUN_ID}/index.html"
+ echo "result-url=${RESULT_URL}" >> $GITHUB_OUTPUT
+
+ - name: Upload Pages artifact
+ uses: actions/upload-pages-artifact@v3
+ with:
+ path: results
+
+ - name: Commit and push to gh-pages
+ run: |
+ git config --global user.name "github-actions[bot]"
+ git config --global user.email "github-actions[bot]@users.noreply.github.com"
+ git add results/ index.html
+ git commit -m "Update benchmark results from run ${{ github.event.workflow_run.id }}"
+ git push origin gh-pages
+
+ comment-pr:
+ needs: build
+ runs-on: ubuntu-latest
+ permissions:
+ pull-requests: write
+ actions: read
+ steps:
+ - name: Comment on PR
+ if: ${{ needs.build.outputs.pr-number != 'main' }}
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ run: |
+ gh pr comment ${{ needs.build.outputs.pr-number }} \
+ --repo ${{ github.repository }} \
+ --body "📊 Benchmark results for this run (${{ github.event.workflow_run.id }}) will be available at: ${{ needs.build.outputs.result-url }} after the github pages \"build and deployment\" action has completed.
+ 🚀 Speedups: ${{ needs.build.outputs.speedups }}"
diff --git a/bench.py b/bench.py
new file mode 100755
index 000000000000..aba690a25ca2
--- /dev/null
+++ b/bench.py
@@ -0,0 +1,529 @@
+#!/usr/bin/env python3
+"""Benchcoin - Bitcoin Core benchmarking toolkit.
+
+A CLI for building, benchmarking, analyzing, and reporting on Bitcoin Core
+performance.
+
+Usage:
+ bench.py build COMMIT[:NAME]... Build bitcoind at one or more commits
+ bench.py run NAME:BINARY... Benchmark one or more binaries
+ bench.py analyze COMMIT LOGFILE Generate plots from debug.log
+ bench.py compare RESULTS... Compare benchmark results
+ bench.py report INPUT OUTPUT Generate HTML report
+
+Examples:
+ # Build two commits
+ bench.py build HEAD~1:before HEAD:after
+
+ # Benchmark built binaries
+ bench.py run before:./binaries/before/bitcoind after:./binaries/after/bitcoind --datadir /data
+
+ # Compare results
+ bench.py compare ./bench-output/results.json
+
+ # Generate HTML report
+ bench.py report ./bench-output ./report
+"""
+
+from __future__ import annotations
+
+import argparse
+import logging
+import sys
+from pathlib import Path
+
+from bench.capabilities import detect_capabilities
+from bench.config import build_config
+
+logging.basicConfig(
+ level=logging.INFO,
+ format="%(levelname)s: %(message)s",
+)
+logger = logging.getLogger(__name__)
+
+
+def cmd_build(args: argparse.Namespace) -> int:
+ """Build bitcoind at one or more commits."""
+ from bench.build import BuildPhase
+
+ capabilities = detect_capabilities()
+ config = build_config(
+ cli_args={
+ "binaries_dir": args.output_dir,
+ "skip_existing": args.skip_existing,
+ "dry_run": args.dry_run,
+ "verbose": args.verbose,
+ },
+ config_file=Path(args.config) if args.config else None,
+ profile=args.profile,
+ )
+
+ if args.verbose:
+ logging.getLogger().setLevel(logging.DEBUG)
+
+ phase = BuildPhase(config, capabilities)
+
+ try:
+ result = phase.run(
+ args.commits,
+ output_dir=Path(args.output_dir) if args.output_dir else None,
+ )
+ logger.info(f"Built {len(result.binaries)} binary(ies):")
+ for binary in result.binaries:
+ logger.info(f" {binary.name}: {binary.path}")
+ return 0
+ except Exception as e:
+ logger.error(f"Build failed: {e}")
+ return 1
+
+
+def cmd_run(args: argparse.Namespace) -> int:
+ """Run benchmark on one or more binaries."""
+ from bench.benchmark import BenchmarkPhase, parse_binary_spec
+
+ capabilities = detect_capabilities()
+ config = build_config(
+ cli_args={
+ "datadir": args.datadir,
+ "tmp_datadir": args.tmp_datadir,
+ "output_dir": args.output_dir,
+ "stop_height": args.stop_height,
+ "dbcache": args.dbcache,
+ "runs": args.runs,
+ "connect": args.connect,
+ "chain": args.chain,
+ "instrumented": args.instrumented,
+ "no_cache_drop": args.no_cache_drop,
+ "dry_run": args.dry_run,
+ "verbose": args.verbose,
+ },
+ config_file=Path(args.config) if args.config else None,
+ profile=args.profile,
+ )
+
+ if args.verbose:
+ logging.getLogger().setLevel(logging.DEBUG)
+
+ errors = config.validate()
+ if errors:
+ for error in errors:
+ logger.error(error)
+ return 1
+
+ # Parse binary specs
+ try:
+ binaries = [parse_binary_spec(spec) for spec in args.binaries]
+ except ValueError as e:
+ logger.error(str(e))
+ return 1
+
+ # Validate binaries exist
+ for name, path in binaries:
+ if not path.exists():
+ logger.error(f"Binary not found: {path} ({name})")
+ return 1
+
+ phase = BenchmarkPhase(config, capabilities)
+ output_dir = Path(config.output_dir)
+
+ try:
+ result = phase.run(
+ binaries=binaries,
+ datadir=Path(config.datadir),
+ output_dir=output_dir,
+ )
+ logger.info(f"Results saved to: {result.results_file}")
+
+ # For instrumented runs, also generate plots
+ if config.instrumented:
+ from bench.analyze import AnalyzePhase
+
+ analyze_phase = AnalyzePhase()
+
+ for binary_result in result.binaries:
+ if binary_result.debug_log:
+ try:
+ analyze_phase.run(
+ commit=binary_result.name,
+ log_file=binary_result.debug_log,
+ output_dir=output_dir / "plots",
+ )
+ except Exception as e:
+ logger.warning(f"Analysis for {binary_result.name} failed: {e}")
+
+ return 0
+ except Exception as e:
+ logger.error(f"Benchmark failed: {e}")
+ if args.verbose:
+ import traceback
+
+ traceback.print_exc()
+ return 1
+
+
+def cmd_compare(args: argparse.Namespace) -> int:
+ """Compare benchmark results from multiple files."""
+ from bench.compare import ComparePhase
+
+ if args.verbose:
+ logging.getLogger().setLevel(logging.DEBUG)
+
+ results_files = [Path(f) for f in args.results_files]
+
+ # Validate files exist
+ for f in results_files:
+ if not f.exists():
+ logger.error(f"Results file not found: {f}")
+ return 1
+
+ phase = ComparePhase()
+
+ try:
+ result = phase.run(results_files, baseline=args.baseline)
+
+ # Output results
+ output_json = phase.to_json(result)
+
+ if args.output:
+ output_path = Path(args.output)
+ output_path.write_text(output_json)
+ logger.info(f"Comparison saved to: {output_path}")
+ else:
+ print(output_json)
+
+ return 0
+ except Exception as e:
+ logger.error(f"Comparison failed: {e}")
+ if args.verbose:
+ import traceback
+
+ traceback.print_exc()
+ return 1
+
+
+def cmd_analyze(args: argparse.Namespace) -> int:
+ """Generate plots from debug.log."""
+ from bench.analyze import AnalyzePhase
+
+ if args.verbose:
+ logging.getLogger().setLevel(logging.DEBUG)
+
+ log_file = Path(args.log_file)
+ output_dir = Path(args.output_dir)
+
+ if not log_file.exists():
+ logger.error(f"Log file not found: {log_file}")
+ return 1
+
+ phase = AnalyzePhase()
+
+ try:
+ result = phase.run(
+ commit=args.commit,
+ log_file=log_file,
+ output_dir=output_dir,
+ )
+ logger.info(f"Generated {len(result.plots)} plots in {result.output_dir}")
+ return 0
+ except Exception as e:
+ logger.error(f"Analysis failed: {e}")
+ if args.verbose:
+ import traceback
+
+ traceback.print_exc()
+ return 1
+
+
+def cmd_report(args: argparse.Namespace) -> int:
+ """Generate HTML report from benchmark results."""
+ from bench.report import ReportPhase
+
+ if args.verbose:
+ logging.getLogger().setLevel(logging.DEBUG)
+
+ output_dir = Path(args.output_dir)
+ phase = ReportPhase()
+
+ try:
+ # CI multi-network mode
+ if args.networks:
+ network_dirs = {}
+ for spec in args.networks:
+ if ":" not in spec:
+ logger.error(f"Invalid network spec '{spec}': must be NETWORK:PATH")
+ return 1
+ network, path = spec.split(":", 1)
+ network_dirs[network] = Path(path)
+
+ # Validate directories exist
+ for network, path in network_dirs.items():
+ if not path.exists():
+ logger.error(f"Network directory not found: {path} ({network})")
+ return 1
+
+ result = phase.run_multi_network(
+ network_dirs=network_dirs,
+ output_dir=output_dir,
+ title=args.title or "Benchmark Results",
+ pr_number=args.pr_number,
+ run_id=args.run_id,
+ )
+
+ # Update main index if we have a results directory
+ if args.update_index:
+ results_base = output_dir.parent.parent # Go up from pr-N/run-id
+ if results_base.exists():
+ phase.update_index(results_base, results_base.parent / "index.html")
+ else:
+ # Standard single-directory mode
+ input_dir = Path(args.input_dir)
+
+ if not input_dir.exists():
+ logger.error(f"Input directory not found: {input_dir}")
+ return 1
+
+ result = phase.run(
+ input_dir=input_dir,
+ output_dir=output_dir,
+ title=args.title or "Benchmark Results",
+ )
+
+ # Print speedups
+ if result.speedups:
+ logger.info("Speedups:")
+ for network, speedup in result.speedups.items():
+ sign = "+" if speedup > 0 else ""
+ logger.info(f" {network}: {sign}{speedup}%")
+
+ return 0
+ except Exception as e:
+ logger.error(f"Report generation failed: {e}")
+ if args.verbose:
+ import traceback
+
+ traceback.print_exc()
+ return 1
+
+
+def main() -> int:
+ """Main entry point."""
+ parser = argparse.ArgumentParser(
+ description="Benchcoin - Bitcoin Core benchmarking toolkit",
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ epilog=__doc__,
+ )
+
+ parser.add_argument(
+ "--config",
+ metavar="PATH",
+ help="Config file (default: bench.toml)",
+ )
+ parser.add_argument(
+ "--profile",
+ choices=["quick", "full", "ci"],
+ default="full",
+ help="Configuration profile (default: full)",
+ )
+ parser.add_argument(
+ "-v",
+ "--verbose",
+ action="store_true",
+ help="Verbose output",
+ )
+ parser.add_argument(
+ "--dry-run",
+ action="store_true",
+ help="Show what would be done without executing",
+ )
+
+ subparsers = parser.add_subparsers(dest="command", help="Commands")
+
+ # Build command
+ build_parser = subparsers.add_parser(
+ "build",
+ help="Build bitcoind at one or more commits",
+ description="Build bitcoind binaries from git commits. "
+ "Each commit can optionally have a name suffix: COMMIT:NAME",
+ )
+ build_parser.add_argument(
+ "commits",
+ nargs="+",
+ metavar="COMMIT[:NAME]",
+ help="Commit(s) to build. Format: COMMIT or COMMIT:NAME (e.g., HEAD:latest, abc123:v27)",
+ )
+ build_parser.add_argument(
+ "-o",
+ "--output-dir",
+ metavar="PATH",
+ help="Where to store binaries (default: ./binaries)",
+ )
+ build_parser.add_argument(
+ "--skip-existing",
+ action="store_true",
+ help="Skip build if binary already exists",
+ )
+ build_parser.set_defaults(func=cmd_build)
+
+ # Run command
+ run_parser = subparsers.add_parser(
+ "run",
+ help="Run benchmark on one or more binaries",
+ description="Benchmark bitcoind binaries using hyperfine. "
+ "Each binary must have a name and path: NAME:PATH",
+ )
+ run_parser.add_argument(
+ "binaries",
+ nargs="+",
+ metavar="NAME:PATH",
+ help="Binary(ies) to benchmark. Format: NAME:PATH (e.g., v27:./binaries/v27/bitcoind)",
+ )
+ run_parser.add_argument(
+ "--datadir",
+ required=True,
+ metavar="PATH",
+ help="Source datadir with blockchain snapshot",
+ )
+ run_parser.add_argument(
+ "--tmp-datadir",
+ metavar="PATH",
+ help="Temp datadir for benchmark runs",
+ )
+ run_parser.add_argument(
+ "-o",
+ "--output-dir",
+ metavar="PATH",
+ help="Output directory for results (default: ./bench-output)",
+ )
+ run_parser.add_argument(
+ "--stop-height",
+ type=int,
+ metavar="N",
+ help="Block height to stop at",
+ )
+ run_parser.add_argument(
+ "--dbcache",
+ type=int,
+ metavar="N",
+ help="Database cache size in MB",
+ )
+ run_parser.add_argument(
+ "--runs",
+ type=int,
+ metavar="N",
+ help="Number of benchmark iterations",
+ )
+ run_parser.add_argument(
+ "--connect",
+ metavar="ADDR",
+ help="Connect address for sync",
+ )
+ run_parser.add_argument(
+ "--chain",
+ choices=["main", "testnet", "signet", "regtest"],
+ help="Chain to use",
+ )
+ run_parser.add_argument(
+ "--instrumented",
+ action="store_true",
+ help="Enable profiling (flamegraph + debug logging)",
+ )
+ run_parser.add_argument(
+ "--no-cache-drop",
+ action="store_true",
+ help="Skip cache dropping between runs",
+ )
+ run_parser.set_defaults(func=cmd_run)
+
+ # Analyze command
+ analyze_parser = subparsers.add_parser(
+ "analyze", help="Generate plots from debug.log"
+ )
+ analyze_parser.add_argument("commit", help="Commit hash (for naming)")
+ analyze_parser.add_argument("log_file", help="Path to debug.log")
+ analyze_parser.add_argument(
+ "--output-dir",
+ default="./plots",
+ metavar="PATH",
+ help="Output directory for plots",
+ )
+ analyze_parser.set_defaults(func=cmd_analyze)
+
+ # Compare command
+ compare_parser = subparsers.add_parser(
+ "compare",
+ help="Compare benchmark results from multiple files",
+ description="Load and compare results from one or more results.json files. "
+ "Calculates speedup percentages relative to a baseline.",
+ )
+ compare_parser.add_argument(
+ "results_files",
+ nargs="+",
+ metavar="RESULTS_FILE",
+ help="results.json file(s) to compare",
+ )
+ compare_parser.add_argument(
+ "--baseline",
+ metavar="NAME",
+ help="Name of the baseline entry (default: first entry)",
+ )
+ compare_parser.add_argument(
+ "-o",
+ "--output",
+ metavar="FILE",
+ help="Output file for comparison JSON (default: stdout)",
+ )
+ compare_parser.set_defaults(func=cmd_compare)
+
+ # Report command
+ report_parser = subparsers.add_parser(
+ "report",
+ help="Generate HTML report",
+ description="Generate HTML report from benchmark results. "
+ "Use --network for multi-network CI reports.",
+ )
+ report_parser.add_argument(
+ "input_dir",
+ nargs="?",
+ help="Directory with results.json (for single-network mode)",
+ )
+ report_parser.add_argument("output_dir", help="Output directory for report")
+ report_parser.add_argument(
+ "--title",
+ help="Report title",
+ )
+ # CI multi-network options
+ report_parser.add_argument(
+ "--network",
+ dest="networks",
+ action="append",
+ metavar="NAME:PATH",
+ help="Network results directory (repeatable, e.g., --network mainnet:./mainnet-results)",
+ )
+ report_parser.add_argument(
+ "--pr-number",
+ metavar="N",
+ help="PR number (for CI reports)",
+ )
+ report_parser.add_argument(
+ "--run-id",
+ metavar="ID",
+ help="Run ID (for CI reports)",
+ )
+ report_parser.add_argument(
+ "--update-index",
+ action="store_true",
+ help="Update main index.html (for CI reports)",
+ )
+ report_parser.set_defaults(func=cmd_report)
+
+ args = parser.parse_args()
+
+ if not args.command:
+ parser.print_help()
+ return 1
+
+ return args.func(args)
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/bench.toml b/bench.toml
new file mode 100644
index 000000000000..9a61bd8048f0
--- /dev/null
+++ b/bench.toml
@@ -0,0 +1,30 @@
+# Benchcoin configuration
+# Values here override built-in defaults but are overridden by environment
+# variables (BENCH_*) and CLI arguments.
+
+[defaults]
+chain = "main"
+dbcache = 450
+stop_height = 855000
+runs = 3
+# connect = "" # Empty or omit to use public P2P network
+
+[paths]
+binaries_dir = "./binaries"
+output_dir = "./bench-output"
+
+# Profiles override specific defaults
+# Usage: bench.py --profile quick full HEAD~1 HEAD
+
+[profiles.quick]
+stop_height = 2000
+runs = 3
+
+[profiles.full]
+stop_height = 855000
+runs = 3
+
+[profiles.ci]
+stop_height = 855000
+runs = 3
+connect = "148.251.128.115:33333"
diff --git a/bench/README.md b/bench/README.md
new file mode 100644
index 000000000000..ca0d011303de
--- /dev/null
+++ b/bench/README.md
@@ -0,0 +1,234 @@
+# Benchcoin
+
+A CLI for benchmarking Bitcoin Core IBD.
+
+## Quick Start
+
+```bash
+# Quick smoke test on signet (requires nix)
+nix develop --command python3 bench.py --profile quick full \
+ --chain signet --datadir /path/to/signet/datadir HEAD~1 HEAD
+
+# Or use just (wraps nix develop)
+just quick HEAD~1 HEAD /path/to/signet/datadir
+```
+
+## Requirements
+
+- **Nix** with flakes enabled (provides hyperfine, flamegraph, etc.)
+- A blockchain datadir snapshot to benchmark against
+- Two git commits to compare
+
+Optional (auto-detected, gracefully degrades without):
+- `/run/wrappers/bin/drop-caches` (NixOS) - clears page cache between runs
+
+## Commands
+
+```
+bench.py [GLOBAL_OPTIONS] COMMAND [OPTIONS] ARGS
+
+Global Options:
+ --profile {quick,full,ci} Configuration profile
+ --config PATH Custom config file
+ -v, --verbose Verbose output
+ --dry-run Show what would run
+
+Commands:
+ build Build bitcoind at two commits
+ run Run benchmark (requires pre-built binaries)
+ analyze Generate plots from debug.log
+ report Generate HTML report
+ full Complete pipeline: build → run → analyze
+```
+
+### build
+
+Build bitcoind binaries at two commits for comparison:
+
+```bash
+python3 bench.py build HEAD~1 HEAD
+python3 bench.py build --binaries-dir /tmp/bins abc123 def456
+python3 bench.py build --skip-existing HEAD~1 HEAD # reuse existing
+```
+
+### run
+
+Run hyperfine benchmark comparing two pre-built binaries:
+
+```bash
+python3 bench.py run --datadir /data/snapshot HEAD~1 HEAD
+python3 bench.py run --instrumented --datadir /data/snapshot HEAD~1 HEAD
+```
+
+Options:
+- `--datadir PATH` - Source blockchain snapshot (required)
+- `--tmp-datadir PATH` - Working directory (default: ./bench-output/tmp-datadir)
+- `--stop-height N` - Block height to sync to
+- `--dbcache N` - Database cache in MB
+- `--runs N` - Number of iterations (default: 3, forced to 1 if instrumented)
+- `--instrumented` - Enable flamegraph profiling and debug logging
+- `--connect ADDR` - P2P node to sync from (empty = public network)
+- `--chain {main,signet,testnet,regtest}` - Which chain
+- `--no-cache-drop` - Don't clear page cache between runs
+
+### analyze
+
+Generate plots from a debug.log file:
+
+```bash
+python3 bench.py analyze abc123 /path/to/debug.log --output-dir ./plots
+```
+
+Generates PNG plots for:
+- Block height vs time
+- Cache size vs height/time
+- Transaction count vs height
+- LevelDB compaction events
+- CoinDB write batches
+
+### report
+
+Generate HTML report from benchmark results:
+
+```bash
+python3 bench.py report ./bench-output ./report
+```
+
+### full
+
+Run complete pipeline (build + run + analyze if instrumented):
+
+```bash
+python3 bench.py --profile quick full --chain signet --datadir /tmp/signet HEAD~1 HEAD
+python3 bench.py --profile full full --datadir /data/mainnet HEAD~1 HEAD
+```
+
+## Profiles
+
+Profiles set sensible defaults for common scenarios:
+
+| Profile | stop_height | runs | dbcache | connect |
+|---------|-------------|------|---------|---------|
+| quick | 1,500 | 1 | 450 | (public network) |
+| full | 855,000 | 3 | 450 | (public network) |
+| ci | 855,000 | 3 | 450 | 148.251.128.115:33333 |
+
+Override any profile setting with CLI flags:
+
+```bash
+python3 bench.py --profile quick full --stop-height 5000 --datadir ... HEAD~1 HEAD
+```
+
+## Configuration
+
+Configuration is layered (lowest to highest priority):
+
+1. Built-in defaults
+2. `bench.toml` (in repo root)
+3. Environment variables (`BENCH_DATADIR`, `BENCH_DBCACHE`, etc.)
+4. CLI arguments
+
+### bench.toml
+
+```toml
+[defaults]
+chain = "main"
+dbcache = 450
+stop_height = 855000
+runs = 3
+
+[paths]
+binaries_dir = "./binaries"
+output_dir = "./bench-output"
+
+[profiles.quick]
+stop_height = 1500
+runs = 1
+dbcache = 450
+
+[profiles.ci]
+connect = "148.251.128.115:33333"
+```
+
+### Environment Variables
+
+```bash
+export BENCH_DATADIR=/data/snapshot
+export BENCH_DBCACHE=1000
+export BENCH_STOP_HEIGHT=100000
+```
+
+## Justfile Recipes
+
+The justfile wraps common operations with `nix develop`:
+
+```bash
+just quick HEAD~1 HEAD /path/to/datadir # Quick signet test
+just full HEAD~1 HEAD /path/to/datadir # Full mainnet benchmark
+just instrumented HEAD~1 HEAD /path/to/datadir # With flamegraphs
+just build HEAD~1 HEAD # Build only
+just run HEAD~1 HEAD /path/to/datadir # Run only (binaries must exist)
+```
+
+## Architecture
+
+```
+bench.py CLI entry point (argparse)
+bench/
+├── config.py Layered configuration (TOML + env + CLI)
+├── capabilities.py System capability detection
+├── build.py Build phase (nix build)
+├── benchmark.py Benchmark phase (hyperfine)
+├── analyze.py Plot generation (matplotlib)
+├── report.py HTML report generation
+└── utils.py Git operations, datadir management
+```
+
+### Capability Detection
+
+The tool auto-detects system capabilities and gracefully degrades:
+
+```python
+from bench.capabilities import detect_capabilities
+caps = detect_capabilities()
+# caps.has_hyperfine, caps.can_drop_caches, etc.
+```
+
+Missing optional features emit warnings but don't fail:
+
+```
+WARNING: drop-caches not available - cache won't be cleared between runs
+```
+
+Missing required features (hyperfine, flamegraph for instrumented) cause errors.
+
+### Hyperfine Integration
+
+The benchmark phase generates temporary shell scripts for hyperfine hooks:
+
+- `setup` - Clean tmp datadir (once before all runs)
+- `prepare` - Copy snapshot, drop caches, clean logs (before each run)
+- `cleanup` - Clean tmp datadir (after all runs per command)
+- `conclude` - Collect flamegraph/logs (instrumented only, after each run)
+
+### Instrumented Mode
+
+When `--instrumented` is set:
+
+1. Wraps bitcoind in `flamegraph` for CPU profiling
+2. Enables debug logging: `-debug=coindb -debug=leveldb -debug=bench -debug=validation`
+3. Forces `runs=1` (profiling overhead makes multiple runs pointless)
+4. Generates flamegraph SVGs and performance plots
+
+## CI Integration
+
+GitHub Actions workflows call bench.py directly (already in nix develop):
+
+```yaml
+- run: |
+ nix develop --command python3 bench.py build \
+ --binaries-dir ${{ runner.temp }}/binaries \
+ $BASE_SHA $HEAD_SHA
+```
+
+CI-specific paths and the dedicated sync node are configured via `--profile ci`.
diff --git a/bench/__init__.py b/bench/__init__.py
new file mode 100644
index 000000000000..cb50424b155c
--- /dev/null
+++ b/bench/__init__.py
@@ -0,0 +1,3 @@
+"""Benchcoin - Bitcoin Core benchmarking toolkit."""
+
+__version__ = "0.1.0"
diff --git a/bench/analyze.py b/bench/analyze.py
new file mode 100644
index 000000000000..baedd97d745c
--- /dev/null
+++ b/bench/analyze.py
@@ -0,0 +1,538 @@
+"""Analyze phase - parse debug.log and generate performance plots.
+
+Refactored from bench-ci/parse_and_plot.py for better structure and reusability.
+"""
+
+from __future__ import annotations
+
+import datetime
+import logging
+import re
+from collections import OrderedDict
+from dataclasses import dataclass
+from pathlib import Path
+
+# matplotlib is optional - gracefully handle if not installed
+try:
+ import matplotlib.pyplot as plt
+
+ HAS_MATPLOTLIB = True
+except ImportError:
+ HAS_MATPLOTLIB = False
+
+logger = logging.getLogger(__name__)
+
+# Bitcoin fork heights for plot annotations
+FORK_HEIGHTS = OrderedDict(
+ [
+ ("BIP34", 227931), # Block v2, coinbase includes height
+ ("BIP66", 363725), # Strict DER signatures
+ ("BIP65", 388381), # OP_CHECKLOCKTIMEVERIFY
+ ("CSV", 419328), # BIP68, 112, 113 - OP_CHECKSEQUENCEVERIFY
+ ("Segwit", 481824), # BIP141, 143, 144, 145 - Segregated Witness
+ ("Taproot", 709632), # BIP341, 342 - Schnorr signatures & Taproot
+ ("Halving 1", 210000), # First halving
+ ("Halving 2", 420000), # Second halving
+ ("Halving 3", 630000), # Third halving
+ ("Halving 4", 840000), # Fourth halving
+ ]
+)
+
+FORK_COLORS = {
+ "BIP34": "blue",
+ "BIP66": "blue",
+ "BIP65": "blue",
+ "CSV": "blue",
+ "Segwit": "green",
+ "Taproot": "red",
+ "Halving 1": "purple",
+ "Halving 2": "purple",
+ "Halving 3": "purple",
+ "Halving 4": "purple",
+}
+
+FORK_STYLES = {
+ "BIP34": "--",
+ "BIP66": "--",
+ "BIP65": "--",
+ "CSV": "--",
+ "Segwit": "--",
+ "Taproot": "--",
+ "Halving 1": ":",
+ "Halving 2": ":",
+ "Halving 3": ":",
+ "Halving 4": ":",
+}
+
+
+@dataclass
+class UpdateTipEntry:
+ """Parsed UpdateTip log entry."""
+
+ timestamp: datetime.datetime
+ height: int
+ tx_count: int
+ cache_size_mb: float
+ cache_coins_count: int
+
+
+@dataclass
+class LevelDBCompactEntry:
+ """Parsed LevelDB compaction log entry."""
+
+ timestamp: datetime.datetime
+
+
+@dataclass
+class LevelDBGenTableEntry:
+ """Parsed LevelDB generated table log entry."""
+
+ timestamp: datetime.datetime
+ keys_count: int
+ bytes_count: int
+
+
+@dataclass
+class ValidationTxAddEntry:
+ """Parsed validation transaction added log entry."""
+
+ timestamp: datetime.datetime
+
+
+@dataclass
+class CoinDBWriteBatchEntry:
+ """Parsed coindb write batch log entry."""
+
+ timestamp: datetime.datetime
+ is_partial: bool
+ size_mb: float
+
+
+@dataclass
+class CoinDBCommitEntry:
+ """Parsed coindb commit log entry."""
+
+ timestamp: datetime.datetime
+ txout_count: int
+
+
+@dataclass
+class ParsedLog:
+ """All parsed data from a debug.log file."""
+
+ update_tip: list[UpdateTipEntry]
+ leveldb_compact: list[LevelDBCompactEntry]
+ leveldb_gen_table: list[LevelDBGenTableEntry]
+ validation_txadd: list[ValidationTxAddEntry]
+ coindb_write_batch: list[CoinDBWriteBatchEntry]
+ coindb_commit: list[CoinDBCommitEntry]
+
+
+@dataclass
+class AnalyzeResult:
+ """Result of the analyze phase."""
+
+ commit: str
+ output_dir: Path
+ plots: list[Path]
+
+
+class LogParser:
+ """Parse bitcoind debug.log files."""
+
+ # Regex patterns
+ UPDATETIP_RE = re.compile(
+ r"^([\d\-:TZ]+) UpdateTip: new best.+height=(\d+).+tx=(\d+).+cache=([\d.]+)MiB\((\d+)txo\)"
+ )
+ LEVELDB_COMPACT_RE = re.compile(r"^([\d\-:TZ]+) \[leveldb] Compacting.*files")
+ LEVELDB_GEN_TABLE_RE = re.compile(
+ r"^([\d\-:TZ]+) \[leveldb] Generated table.*: (\d+) keys, (\d+) bytes"
+ )
+ VALIDATION_TXADD_RE = re.compile(
+ r"^([\d\-:TZ]+) \[validation] TransactionAddedToMempool: txid=.+wtxid=.+"
+ )
+ COINDB_WRITE_BATCH_RE = re.compile(
+ r"^([\d\-:TZ]+) \[coindb] Writing (partial|final) batch of ([\d.]+) MiB"
+ )
+ COINDB_COMMIT_RE = re.compile(
+ r"^([\d\-:TZ]+) \[coindb] Committed (\d+) changed transaction outputs"
+ )
+
+ @staticmethod
+ def parse_timestamp(iso_str: str) -> datetime.datetime:
+ """Parse ISO 8601 timestamp from log."""
+ return datetime.datetime.strptime(iso_str, "%Y-%m-%dT%H:%M:%SZ")
+
+ def parse_file(self, log_file: Path) -> ParsedLog:
+ """Parse a debug.log file and extract all relevant data."""
+ update_tip: list[UpdateTipEntry] = []
+ leveldb_compact: list[LevelDBCompactEntry] = []
+ leveldb_gen_table: list[LevelDBGenTableEntry] = []
+ validation_txadd: list[ValidationTxAddEntry] = []
+ coindb_write_batch: list[CoinDBWriteBatchEntry] = []
+ coindb_commit: list[CoinDBCommitEntry] = []
+
+ with open(log_file, "r", encoding="utf-8") as f:
+ for line in f:
+ if match := self.UPDATETIP_RE.match(line):
+ iso_str, height, tx, cache_mb, coins = match.groups()
+ update_tip.append(
+ UpdateTipEntry(
+ timestamp=self.parse_timestamp(iso_str),
+ height=int(height),
+ tx_count=int(tx),
+ cache_size_mb=float(cache_mb),
+ cache_coins_count=int(coins),
+ )
+ )
+ elif match := self.LEVELDB_COMPACT_RE.match(line):
+ leveldb_compact.append(
+ LevelDBCompactEntry(
+ timestamp=self.parse_timestamp(match.group(1))
+ )
+ )
+ elif match := self.LEVELDB_GEN_TABLE_RE.match(line):
+ iso_str, keys, bytes_count = match.groups()
+ leveldb_gen_table.append(
+ LevelDBGenTableEntry(
+ timestamp=self.parse_timestamp(iso_str),
+ keys_count=int(keys),
+ bytes_count=int(bytes_count),
+ )
+ )
+ elif match := self.VALIDATION_TXADD_RE.match(line):
+ validation_txadd.append(
+ ValidationTxAddEntry(
+ timestamp=self.parse_timestamp(match.group(1))
+ )
+ )
+ elif match := self.COINDB_WRITE_BATCH_RE.match(line):
+ iso_str, batch_type, size_mb = match.groups()
+ coindb_write_batch.append(
+ CoinDBWriteBatchEntry(
+ timestamp=self.parse_timestamp(iso_str),
+ is_partial=(batch_type == "partial"),
+ size_mb=float(size_mb),
+ )
+ )
+ elif match := self.COINDB_COMMIT_RE.match(line):
+ iso_str, txout_count = match.groups()
+ coindb_commit.append(
+ CoinDBCommitEntry(
+ timestamp=self.parse_timestamp(iso_str),
+ txout_count=int(txout_count),
+ )
+ )
+
+ return ParsedLog(
+ update_tip=update_tip,
+ leveldb_compact=leveldb_compact,
+ leveldb_gen_table=leveldb_gen_table,
+ validation_txadd=validation_txadd,
+ coindb_write_batch=coindb_write_batch,
+ coindb_commit=coindb_commit,
+ )
+
+
+class PlotGenerator:
+ """Generate performance plots from parsed log data."""
+
+ def __init__(self, commit: str, output_dir: Path):
+ self.commit = commit
+ self.output_dir = output_dir
+ self.generated_plots: list[Path] = []
+
+ if not HAS_MATPLOTLIB:
+ raise RuntimeError(
+ "matplotlib is required for plot generation. "
+ "Install with: pip install matplotlib"
+ )
+
+ def generate_all(self, data: ParsedLog) -> list[Path]:
+ """Generate all plots from parsed data."""
+ if not data.update_tip:
+ logger.warning("No UpdateTip entries found, skipping plot generation")
+ return []
+
+ # Verify entries are sorted by time
+ for i in range(len(data.update_tip) - 1):
+ if data.update_tip[i].timestamp > data.update_tip[i + 1].timestamp:
+ logger.warning("UpdateTip entries are not sorted by time")
+ break
+
+ # Extract base time for elapsed calculations
+ base_time = data.update_tip[0].timestamp
+
+ # Extract data series
+ times = [e.timestamp for e in data.update_tip]
+ heights = [e.height for e in data.update_tip]
+ tx_counts = [e.tx_count for e in data.update_tip]
+ cache_sizes = [e.cache_size_mb for e in data.update_tip]
+ cache_counts = [e.cache_coins_count for e in data.update_tip]
+ elapsed_minutes = [(t - base_time).total_seconds() / 60 for t in times]
+
+ # Generate core plots
+ self._plot(
+ elapsed_minutes,
+ heights,
+ "Elapsed minutes",
+ "Block Height",
+ "Block Height vs Time",
+ f"{self.commit}-height_vs_time.png",
+ )
+
+ self._plot(
+ heights,
+ cache_sizes,
+ "Block Height",
+ "Cache Size (MiB)",
+ "Cache Size vs Block Height",
+ f"{self.commit}-cache_vs_height.png",
+ is_height_based=True,
+ )
+
+ self._plot(
+ elapsed_minutes,
+ cache_sizes,
+ "Elapsed minutes",
+ "Cache Size (MiB)",
+ "Cache Size vs Time",
+ f"{self.commit}-cache_vs_time.png",
+ )
+
+ self._plot(
+ heights,
+ tx_counts,
+ "Block Height",
+ "Transaction Count",
+ "Transactions vs Block Height",
+ f"{self.commit}-tx_vs_height.png",
+ is_height_based=True,
+ )
+
+ self._plot(
+ heights,
+ cache_counts,
+ "Block Height",
+ "Coins Cache Size",
+ "Coins Cache Size vs Height",
+ f"{self.commit}-coins_cache_vs_height.png",
+ is_height_based=True,
+ )
+
+ # LevelDB plots
+ if data.leveldb_compact:
+ compact_minutes = [
+ (e.timestamp - base_time).total_seconds() / 60
+ for e in data.leveldb_compact
+ ]
+ self._plot(
+ compact_minutes,
+ [1] * len(compact_minutes),
+ "Elapsed minutes",
+ "LevelDB Compaction",
+ "LevelDB Compaction Events vs Time",
+ f"{self.commit}-leveldb_compact_vs_time.png",
+ )
+
+ if data.leveldb_gen_table:
+ gen_minutes = [
+ (e.timestamp - base_time).total_seconds() / 60
+ for e in data.leveldb_gen_table
+ ]
+ gen_keys = [e.keys_count for e in data.leveldb_gen_table]
+ gen_bytes = [e.bytes_count for e in data.leveldb_gen_table]
+
+ self._plot(
+ gen_minutes,
+ gen_keys,
+ "Elapsed minutes",
+ "Number of keys",
+ "LevelDB Keys Generated vs Time",
+ f"{self.commit}-leveldb_gen_keys_vs_time.png",
+ )
+
+ self._plot(
+ gen_minutes,
+ gen_bytes,
+ "Elapsed minutes",
+ "Number of bytes",
+ "LevelDB Bytes Generated vs Time",
+ f"{self.commit}-leveldb_gen_bytes_vs_time.png",
+ )
+
+ # Validation plots
+ if data.validation_txadd:
+ txadd_minutes = [
+ (e.timestamp - base_time).total_seconds() / 60
+ for e in data.validation_txadd
+ ]
+ self._plot(
+ txadd_minutes,
+ [1] * len(txadd_minutes),
+ "Elapsed minutes",
+ "Transaction Additions",
+ "Transaction Additions to Mempool vs Time",
+ f"{self.commit}-validation_txadd_vs_time.png",
+ )
+
+ # CoinDB plots
+ if data.coindb_write_batch:
+ batch_minutes = [
+ (e.timestamp - base_time).total_seconds() / 60
+ for e in data.coindb_write_batch
+ ]
+ batch_sizes = [e.size_mb for e in data.coindb_write_batch]
+ self._plot(
+ batch_minutes,
+ batch_sizes,
+ "Elapsed minutes",
+ "Batch Size MiB",
+ "Coin Database Partial/Final Write Batch Size vs Time",
+ f"{self.commit}-coindb_write_batch_size_vs_time.png",
+ )
+
+ if data.coindb_commit:
+ commit_minutes = [
+ (e.timestamp - base_time).total_seconds() / 60
+ for e in data.coindb_commit
+ ]
+ commit_txouts = [e.txout_count for e in data.coindb_commit]
+ self._plot(
+ commit_minutes,
+ commit_txouts,
+ "Elapsed minutes",
+ "Transaction Output Count",
+ "Coin Database Transaction Output Committed vs Time",
+ f"{self.commit}-coindb_commit_txout_vs_time.png",
+ )
+
+ return self.generated_plots
+
+ def _plot(
+ self,
+ x: list,
+ y: list,
+ x_label: str,
+ y_label: str,
+ title: str,
+ filename: str,
+ is_height_based: bool = False,
+ ) -> None:
+ """Generate a single plot."""
+ if not x or not y:
+ logger.debug(f"Skipping plot '{title}' - no data")
+ return
+
+ plt.figure(figsize=(30, 10))
+ plt.plot(x, y)
+ plt.title(title, fontsize=20)
+ plt.xlabel(x_label, fontsize=16)
+ plt.ylabel(y_label, fontsize=16)
+ plt.grid(True)
+
+ min_x, max_x = min(x), max(x)
+ if min_x < max_x:
+ plt.xlim(min_x, max_x)
+
+ # Add fork markers for height-based plots
+ if is_height_based:
+ self._add_fork_markers(min_x, max_x, max(y))
+
+ plt.xticks(rotation=90, fontsize=12)
+ plt.yticks(fontsize=12)
+ plt.tight_layout()
+
+ output_path = self.output_dir / filename
+ plt.savefig(output_path)
+ plt.close()
+
+ self.generated_plots.append(output_path)
+ logger.info(f"Saved plot: {output_path}")
+
+ def _add_fork_markers(self, min_x: float, max_x: float, max_y: float) -> None:
+ """Add vertical lines for Bitcoin forks."""
+ text_positions = {}
+ position_increment = max_y * 0.05
+ current_position = max_y * 0.9
+
+ for fork_name, height in FORK_HEIGHTS.items():
+ if min_x <= height <= max_x:
+ plt.axvline(
+ x=height,
+ color=FORK_COLORS[fork_name],
+ linestyle=FORK_STYLES[fork_name],
+ )
+
+ if height in text_positions:
+ text_positions[height] -= position_increment
+ else:
+ text_positions[height] = current_position
+ current_position -= position_increment
+ if current_position < max_y * 0.1:
+ current_position = max_y * 0.9
+
+ plt.text(
+ height,
+ text_positions[height],
+ f"{fork_name} ({height})",
+ rotation=90,
+ verticalalignment="top",
+ color=FORK_COLORS[fork_name],
+ )
+
+
+class AnalyzePhase:
+ """Analyze benchmark results and generate plots."""
+
+ def run(
+ self,
+ commit: str,
+ log_file: Path,
+ output_dir: Path,
+ ) -> AnalyzeResult:
+ """Analyze a debug.log and generate plots.
+
+ Args:
+ commit: Commit hash (for naming)
+ log_file: Path to debug.log
+ output_dir: Where to save plots
+
+ Returns:
+ AnalyzeResult with paths to generated plots
+ """
+ if not HAS_MATPLOTLIB:
+ raise RuntimeError(
+ "matplotlib is required for plot generation. "
+ "Install with: pip install matplotlib"
+ )
+
+ if not log_file.exists():
+ raise FileNotFoundError(f"Log file not found: {log_file}")
+
+ output_dir.mkdir(parents=True, exist_ok=True)
+
+ logger.info(f"Parsing log file: {log_file}")
+ parser = LogParser()
+ data = parser.parse_file(log_file)
+
+ # Log parsed data summary
+ logger.info(f" UpdateTip entries: {len(data.update_tip)}")
+ logger.info(f" LevelDB compact entries: {len(data.leveldb_compact)}")
+ logger.info(f" LevelDB gen table entries: {len(data.leveldb_gen_table)}")
+ logger.info(f" Validation txadd entries: {len(data.validation_txadd)}")
+ logger.info(f" CoinDB write batch entries: {len(data.coindb_write_batch)}")
+ logger.info(f" CoinDB commit entries: {len(data.coindb_commit)}")
+
+ logger.info(f"Generating plots for {commit[:12]}")
+ logger.info(f" Output directory: {output_dir}")
+ generator = PlotGenerator(commit[:12], output_dir)
+ plots = generator.generate_all(data)
+
+ logger.info(f"Generated {len(plots)} plots")
+
+ return AnalyzeResult(
+ commit=commit,
+ output_dir=output_dir,
+ plots=plots,
+ )
diff --git a/bench/benchmark.py b/bench/benchmark.py
new file mode 100644
index 000000000000..788e4e53e94d
--- /dev/null
+++ b/bench/benchmark.py
@@ -0,0 +1,349 @@
+"""Benchmark phase - run hyperfine benchmarks on bitcoind binaries."""
+
+from __future__ import annotations
+
+import logging
+import os
+import shutil
+import subprocess
+import tempfile
+from dataclasses import dataclass, field
+from pathlib import Path
+from typing import TYPE_CHECKING
+
+from .patchelf import ensure_binary_runnable
+
+if TYPE_CHECKING:
+ from .capabilities import Capabilities
+ from .config import Config
+
+
+logger = logging.getLogger(__name__)
+
+# Debug flags for instrumented mode
+INSTRUMENTED_DEBUG_FLAGS = ["coindb", "leveldb", "bench", "validation"]
+
+
+@dataclass
+class BinaryResult:
+ """Result for a single binary."""
+
+ name: str
+ flamegraph: Path | None = None
+ debug_log: Path | None = None
+
+
+@dataclass
+class BenchmarkResult:
+ """Result of the benchmark phase."""
+
+ results_file: Path
+ instrumented: bool
+ binaries: list[BinaryResult] = field(default_factory=list)
+
+
+def parse_binary_spec(spec: str) -> tuple[str, Path]:
+ """Parse a binary spec like 'name:/path/to/binary'.
+
+ Returns (name, path).
+ """
+ if ":" not in spec:
+ raise ValueError(f"Invalid binary spec '{spec}': must be NAME:PATH")
+ name, path_str = spec.split(":", 1)
+ if not name:
+ raise ValueError(f"Invalid binary spec '{spec}': name cannot be empty")
+ return name, Path(path_str)
+
+
+class BenchmarkPhase:
+ """Run hyperfine benchmarks on bitcoind binaries."""
+
+ def __init__(
+ self,
+ config: Config,
+ capabilities: Capabilities,
+ ):
+ self.config = config
+ self.capabilities = capabilities
+ self._temp_scripts: list[Path] = []
+
+ def run(
+ self,
+ binaries: list[tuple[str, Path]],
+ datadir: Path,
+ output_dir: Path,
+ ) -> BenchmarkResult:
+ """Run benchmarks on given binaries.
+
+ Args:
+ binaries: List of (name, binary_path) tuples
+ datadir: Source datadir with blockchain snapshot
+ output_dir: Where to store results
+
+ Returns:
+ BenchmarkResult with paths to outputs
+ """
+ if not binaries:
+ raise ValueError("At least one binary is required")
+
+ # Validate all binaries exist
+ for name, path in binaries:
+ if not path.exists():
+ raise FileNotFoundError(f"Binary not found: {path} ({name})")
+
+ # Ensure binaries can run on this system (patches guix binaries on NixOS)
+ for name, path in binaries:
+ if not ensure_binary_runnable(path):
+ raise RuntimeError(f"Binary {name} at {path} cannot be made runnable")
+
+ # Check prerequisites
+ errors = self.capabilities.check_for_run(self.config.instrumented)
+ if errors:
+ raise RuntimeError("Benchmark prerequisites not met:\n" + "\n".join(errors))
+
+ # Log warnings about missing optional capabilities
+ for warning in self.capabilities.get_warnings():
+ logger.warning(warning)
+
+ # Setup directories
+ output_dir.mkdir(parents=True, exist_ok=True)
+ tmp_datadir = Path(self.config.tmp_datadir)
+ tmp_datadir.mkdir(parents=True, exist_ok=True)
+
+ results_file = output_dir / "results.json"
+
+ logger.info("Starting benchmark")
+ logger.info(f" Output dir: {output_dir}")
+ logger.info(f" Temp datadir: {tmp_datadir}")
+ logger.info(f" Source datadir: {datadir}")
+ logger.info(f" Binaries: {len(binaries)}")
+ for name, path in binaries:
+ logger.info(f" {name}: {path}")
+ logger.info(f" Instrumented: {self.config.instrumented}")
+ logger.info(f" Runs: {self.config.runs}")
+ logger.info(f" Stop height: {self.config.stop_height}")
+ logger.info(f" dbcache: {self.config.dbcache}")
+
+ try:
+ # Create hook scripts for hyperfine
+ setup_script = self._create_setup_script(tmp_datadir)
+ prepare_script = self._create_prepare_script(tmp_datadir, datadir)
+ cleanup_script = self._create_cleanup_script(tmp_datadir)
+
+ # Build hyperfine command
+ cmd = self._build_hyperfine_cmd(
+ binaries=binaries,
+ tmp_datadir=tmp_datadir,
+ results_file=results_file,
+ setup_script=setup_script,
+ prepare_script=prepare_script,
+ cleanup_script=cleanup_script,
+ output_dir=output_dir,
+ )
+
+ # Log the commands being benchmarked
+ logger.info("Commands to benchmark:")
+ for name, path in binaries:
+ bitcoind_cmd = self._build_bitcoind_cmd(path, tmp_datadir)
+ logger.info(f" {name}: {bitcoind_cmd}")
+
+ if self.config.dry_run:
+ logger.info(f"[DRY RUN] Would run: {' '.join(cmd)}")
+ return BenchmarkResult(
+ results_file=results_file,
+ instrumented=self.config.instrumented,
+ )
+
+ # Log the full hyperfine command
+ logger.info("Running hyperfine...")
+ logger.info(f" Command: {' '.join(cmd[:7])} ...") # First few args
+ logger.debug(f" Full command: {' '.join(cmd)}")
+ subprocess.run(cmd, check=True)
+
+ # Collect results
+ benchmark_result = BenchmarkResult(
+ results_file=results_file,
+ instrumented=self.config.instrumented,
+ )
+
+ # For instrumented runs, collect flamegraphs and debug logs
+ if self.config.instrumented:
+ logger.info("Collecting instrumented artifacts...")
+ for name, _path in binaries:
+ binary_result = BinaryResult(name=name)
+
+ flamegraph_file = output_dir / f"{name}-flamegraph.svg"
+ debug_log_file = output_dir / f"{name}-debug.log"
+
+ if flamegraph_file.exists():
+ binary_result.flamegraph = flamegraph_file
+ logger.info(f" Flamegraph ({name}): {flamegraph_file}")
+ if debug_log_file.exists():
+ binary_result.debug_log = debug_log_file
+ logger.info(f" Debug log ({name}): {debug_log_file}")
+
+ benchmark_result.binaries.append(binary_result)
+
+ # Clean up tmp_datadir
+ if tmp_datadir.exists():
+ logger.debug(f"Cleaning up tmp_datadir: {tmp_datadir}")
+ shutil.rmtree(tmp_datadir)
+
+ return benchmark_result
+
+ finally:
+ # Clean up temp scripts
+ for script in self._temp_scripts:
+ if script.exists():
+ script.unlink()
+ self._temp_scripts.clear()
+
+ def _create_temp_script(self, commands: list[str], name: str) -> Path:
+ """Create a temporary shell script."""
+ content = "#!/usr/bin/env bash\nset -euxo pipefail\n"
+ content += "\n".join(commands) + "\n"
+
+ fd, path = tempfile.mkstemp(suffix=".sh", prefix=f"bench_{name}_")
+ os.write(fd, content.encode())
+ os.close(fd)
+ os.chmod(path, 0o755)
+
+ script_path = Path(path)
+ self._temp_scripts.append(script_path)
+ logger.debug(f"Created {name} script: {script_path}")
+ for cmd in commands:
+ logger.debug(f" {cmd}")
+ return script_path
+
+ def _create_setup_script(self, tmp_datadir: Path) -> Path:
+ """Create setup script (runs once before all timing runs)."""
+ commands = [
+ f'mkdir -p "{tmp_datadir}"',
+ f'rm -rf "{tmp_datadir}"/*',
+ ]
+ return self._create_temp_script(commands, "setup")
+
+ def _create_prepare_script(self, tmp_datadir: Path, original_datadir: Path) -> Path:
+ """Create prepare script (runs before each timing run)."""
+ commands = [
+ f'rm -rf "{tmp_datadir}"/*',
+ ]
+
+ # Copy datadir
+ commands.append(f'cp -r "{original_datadir}"/* "{tmp_datadir}"')
+
+ # Drop caches if available
+ if self.capabilities.can_drop_caches and not self.config.no_cache_drop:
+ commands.append(self.capabilities.drop_caches_path)
+
+ # Clean debug logs
+ commands.append(
+ f'find "{tmp_datadir}" -name debug.log -delete 2>/dev/null || true'
+ )
+
+ return self._create_temp_script(commands, "prepare")
+
+ def _create_cleanup_script(self, tmp_datadir: Path) -> Path:
+ """Create cleanup script (runs after all timing runs for each command)."""
+ commands = [
+ f'rm -rf "{tmp_datadir}"/*',
+ ]
+ return self._create_temp_script(commands, "cleanup")
+
+ def _build_bitcoind_cmd(
+ self,
+ binary: Path,
+ tmp_datadir: Path,
+ ) -> str:
+ """Build the bitcoind command string for hyperfine."""
+ parts = []
+
+ # Add flamegraph wrapper for instrumented mode
+ if self.config.instrumented:
+ parts.append("flamegraph")
+ parts.append("--palette bitcoin")
+ parts.append("--title 'bitcoind IBD'")
+ parts.append("-c 'record -F 101 --call-graph fp'")
+ parts.append("--")
+
+ # Bitcoind command
+ parts.append(str(binary))
+ parts.append(f"-datadir={tmp_datadir}")
+ parts.append(f"-dbcache={self.config.dbcache}")
+ parts.append(f"-stopatheight={self.config.stop_height}")
+ parts.append("-prune=10000")
+ parts.append(f"-chain={self.config.chain}")
+ parts.append("-daemon=0")
+ parts.append("-printtoconsole=0")
+
+ if self.config.connect:
+ parts.append(f"-connect={self.config.connect}")
+
+ # Debug flags for instrumented mode
+ if self.config.instrumented:
+ for flag in INSTRUMENTED_DEBUG_FLAGS:
+ parts.append(f"-debug={flag}")
+
+ return " ".join(parts)
+
+ def _build_hyperfine_cmd(
+ self,
+ binaries: list[tuple[str, Path]],
+ tmp_datadir: Path,
+ results_file: Path,
+ setup_script: Path,
+ prepare_script: Path,
+ cleanup_script: Path,
+ output_dir: Path,
+ ) -> list[str]:
+ """Build the hyperfine command."""
+ cmd = [
+ "hyperfine",
+ "--shell=bash",
+ f"--setup={setup_script}",
+ f"--prepare={prepare_script}",
+ f"--cleanup={cleanup_script}",
+ f"--runs={self.config.runs}",
+ f"--export-json={results_file}",
+ "--show-output",
+ ]
+
+ # Add command names and build commands
+ for name, binary_path in binaries:
+ cmd.append(f"--command-name={name}")
+
+ # Build the actual commands to benchmark
+ for name, binary_path in binaries:
+ bitcoind_cmd = self._build_bitcoind_cmd(binary_path, tmp_datadir)
+
+ # For instrumented runs, append the conclude logic to each command
+ if self.config.instrumented:
+ conclude = self._create_conclude_commands(name, tmp_datadir, output_dir)
+ bitcoind_cmd += f" && {conclude}"
+
+ cmd.append(bitcoind_cmd)
+
+ return cmd
+
+ def _create_conclude_commands(
+ self,
+ name: str,
+ tmp_datadir: Path,
+ output_dir: Path,
+ ) -> str:
+ """Create inline conclude commands for a specific binary."""
+ # Return shell commands to run after each benchmark
+ commands = []
+
+ # Move flamegraph if exists
+ commands.append(
+ f'if [ -e flamegraph.svg ]; then mv flamegraph.svg "{output_dir}/{name}-flamegraph.svg"; fi'
+ )
+
+ # Copy debug log if exists
+ commands.append(
+ f'debug_log=$(find "{tmp_datadir}" -name debug.log -print -quit); '
+ f'if [ -n "$debug_log" ]; then cp "$debug_log" "{output_dir}/{name}-debug.log"; fi'
+ )
+
+ return " && ".join(commands)
diff --git a/bench/build.py b/bench/build.py
new file mode 100644
index 000000000000..6187263a73de
--- /dev/null
+++ b/bench/build.py
@@ -0,0 +1,197 @@
+"""Build phase - compile bitcoind at specified commits."""
+
+from __future__ import annotations
+
+import logging
+import shutil
+import subprocess
+from dataclasses import dataclass
+from pathlib import Path
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from .capabilities import Capabilities
+ from .config import Config
+
+from .utils import GitState, git_checkout, git_rev_parse
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass
+class BuiltBinary:
+ """A single built binary."""
+
+ name: str
+ path: Path
+ commit: str
+
+
+@dataclass
+class BuildResult:
+ """Result of the build phase."""
+
+ binaries: list[BuiltBinary]
+
+
+def parse_commit_spec(spec: str) -> tuple[str, str | None]:
+ """Parse a commit spec like 'abc123:name' or 'abc123'.
+
+ Returns (commit, name) where name may be None.
+ """
+ if ":" in spec:
+ commit, name = spec.split(":", 1)
+ return commit, name
+ return spec, None
+
+
+class BuildPhase:
+ """Build bitcoind binaries at specified commits."""
+
+ def __init__(
+ self,
+ config: Config,
+ capabilities: Capabilities,
+ repo_path: Path | None = None,
+ ):
+ self.config = config
+ self.capabilities = capabilities
+ self.repo_path = repo_path or Path.cwd()
+
+ def run(
+ self,
+ commit_specs: list[str],
+ output_dir: Path | None = None,
+ ) -> BuildResult:
+ """Build bitcoind at given commits.
+
+ Args:
+ commit_specs: List of commit specs like 'abc123:name' or 'abc123'
+ output_dir: Where to store binaries (default: ./binaries)
+
+ Returns:
+ BuildResult with list of built binaries
+ """
+ # Check prerequisites
+ errors = self.capabilities.check_for_build()
+ if errors:
+ raise RuntimeError("Build prerequisites not met:\n" + "\n".join(errors))
+
+ output_dir = output_dir or Path(self.config.binaries_dir)
+
+ # Parse commit specs and resolve to full hashes
+ commits: list[tuple[str, str, str]] = [] # (commit_hash, name, original_spec)
+ for spec in commit_specs:
+ commit, name = parse_commit_spec(spec)
+ commit_hash = git_rev_parse(commit, self.repo_path)
+ # Default name to short hash if not provided
+ if name is None:
+ name = commit_hash[:12]
+ commits.append((commit_hash, name, spec))
+
+ logger.info(f"Building {len(commits)} binary(ies):")
+ for commit_hash, name, spec in commits:
+ logger.info(f" {name}: {commit_hash[:12]} ({spec})")
+ logger.info(f" Repo: {self.repo_path}")
+ logger.info(f" Output: {output_dir}")
+
+ # Check if we can skip existing builds
+ binaries_to_build: list[
+ tuple[str, str, Path]
+ ] = [] # (commit_hash, name, output_path)
+ for commit_hash, name, _spec in commits:
+ binary_dir = output_dir / name
+ binary_dir.mkdir(parents=True, exist_ok=True)
+ binary_path = binary_dir / "bitcoind"
+
+ if self.config.skip_existing and binary_path.exists():
+ logger.info(f" Skipping {name} - binary exists")
+ else:
+ binaries_to_build.append((commit_hash, name, binary_path))
+
+ if not binaries_to_build:
+ logger.info("All binaries exist and --skip-existing set, skipping build")
+ return BuildResult(
+ binaries=[
+ BuiltBinary(
+ name=name,
+ path=output_dir / name / "bitcoind",
+ commit=commit_hash,
+ )
+ for commit_hash, name, _spec in commits
+ ]
+ )
+
+ # Save git state for restoration
+ git_state = GitState(self.repo_path)
+ git_state.save()
+
+ built_binaries: list[BuiltBinary] = []
+
+ try:
+ for commit_hash, name, output_path in binaries_to_build:
+ self._build_commit(name, commit_hash, output_path)
+ built_binaries.append(
+ BuiltBinary(name=name, path=output_path, commit=commit_hash)
+ )
+
+ finally:
+ # Always restore git state
+ git_state.restore()
+
+ # Include skipped binaries in result
+ all_binaries = []
+ for commit_hash, name, _spec in commits:
+ binary_path = output_dir / name / "bitcoind"
+ all_binaries.append(
+ BuiltBinary(name=name, path=binary_path, commit=commit_hash)
+ )
+
+ return BuildResult(binaries=all_binaries)
+
+ def _build_commit(self, name: str, commit: str, output_path: Path) -> None:
+ """Build bitcoind for a single commit."""
+ logger.info(f"Building {name} ({commit[:12]})")
+
+ if self.config.dry_run:
+ logger.info(f" [DRY RUN] Would build {commit[:12]} -> {output_path}")
+ return
+
+ # Checkout the commit
+ logger.info(f" Checking out {commit[:12]}...")
+ git_checkout(commit, self.repo_path)
+
+ # Build with nix
+ cmd = ["nix", "build", "-L"]
+
+ logger.info(f" Running: {' '.join(cmd)}")
+ logger.info(f" Working directory: {self.repo_path}")
+ result = subprocess.run(
+ cmd,
+ cwd=self.repo_path,
+ )
+
+ if result.returncode != 0:
+ raise RuntimeError(f"Build failed for {name} ({commit[:12]})")
+
+ # Copy binary to output location
+ nix_binary = self.repo_path / "result" / "bin" / "bitcoind"
+ if not nix_binary.exists():
+ raise RuntimeError(f"Built binary not found at {nix_binary}")
+
+ logger.info(f" Copying {nix_binary} -> {output_path}")
+
+ # Remove existing binary if present (may be read-only from nix)
+ if output_path.exists():
+ output_path.chmod(0o755)
+ output_path.unlink()
+
+ shutil.copy2(nix_binary, output_path)
+ output_path.chmod(0o755) # Ensure it's executable and writable
+ logger.info(f" Built {name} binary: {output_path}")
+
+ # Clean up nix result symlink
+ result_link = self.repo_path / "result"
+ if result_link.is_symlink():
+ logger.debug(f" Removing nix result symlink: {result_link}")
+ result_link.unlink()
diff --git a/bench/capabilities.py b/bench/capabilities.py
new file mode 100644
index 000000000000..31b6bd59f05f
--- /dev/null
+++ b/bench/capabilities.py
@@ -0,0 +1,117 @@
+"""System capability detection for graceful degradation.
+
+Detects available tools and features, allowing the benchmark to run
+on systems without all capabilities (with appropriate warnings).
+"""
+
+from __future__ import annotations
+
+import os
+import shutil
+from dataclasses import dataclass
+from pathlib import Path
+
+
+# Known paths for drop-caches on NixOS
+DROP_CACHES_PATHS = [
+ "/run/wrappers/bin/drop-caches",
+ "/usr/local/bin/drop-caches",
+]
+
+
+@dataclass
+class Capabilities:
+ """Detected system capabilities."""
+
+ # Cache management
+ can_drop_caches: bool
+ drop_caches_path: str | None
+
+ # Required tools
+ has_hyperfine: bool
+ has_flamegraph: bool
+ has_perf: bool
+ has_nix: bool
+
+ # System info
+ cpu_count: int
+ is_nixos: bool
+ is_ci: bool
+
+ def check_for_run(self, instrumented: bool = False) -> list[str]:
+ """Check if we have required capabilities for a benchmark run.
+
+ Returns list of errors (empty if all good).
+ """
+ errors = []
+
+ if not self.has_hyperfine:
+ errors.append("hyperfine not found in PATH (required for benchmarking)")
+
+ if instrumented:
+ if not self.has_flamegraph:
+ errors.append(
+ "flamegraph not found in PATH (required for --instrumented)"
+ )
+ if not self.has_perf:
+ errors.append("perf not found in PATH (required for --instrumented)")
+
+ return errors
+
+ def check_for_build(self) -> list[str]:
+ """Check if we have required capabilities for building.
+
+ Returns list of errors (empty if all good).
+ """
+ errors = []
+
+ if not self.has_nix:
+ errors.append("nix not found in PATH (required for building)")
+
+ return errors
+
+ def get_warnings(self) -> list[str]:
+ """Get warnings about missing optional capabilities."""
+ warnings = []
+
+ if not self.can_drop_caches:
+ warnings.append(
+ "drop-caches not available - cache won't be cleared between runs"
+ )
+
+ return warnings
+
+
+def _check_executable(name: str) -> bool:
+ """Check if an executable is available in PATH."""
+ return shutil.which(name) is not None
+
+
+def _find_drop_caches() -> str | None:
+ """Find drop-caches executable."""
+ for path in DROP_CACHES_PATHS:
+ if Path(path).exists() and os.access(path, os.X_OK):
+ return path
+ return None
+
+
+def _is_nixos() -> bool:
+ """Check if we're running on NixOS."""
+ return Path("/etc/NIXOS").exists()
+
+
+def detect_capabilities() -> Capabilities:
+ """Auto-detect system capabilities."""
+ drop_caches_path = _find_drop_caches()
+
+ return Capabilities(
+ can_drop_caches=drop_caches_path is not None,
+ drop_caches_path=drop_caches_path,
+ has_hyperfine=_check_executable("hyperfine"),
+ has_flamegraph=_check_executable("flamegraph"),
+ has_perf=_check_executable("perf"),
+ has_nix=_check_executable("nix"),
+ cpu_count=os.cpu_count() or 1,
+ is_nixos=_is_nixos(),
+ is_ci=os.environ.get("CI", "").lower() in ("true", "1", "yes"),
+ )
diff --git a/bench/compare.py b/bench/compare.py
new file mode 100644
index 000000000000..fac328841634
--- /dev/null
+++ b/bench/compare.py
@@ -0,0 +1,180 @@
+"""Compare phase - compare benchmark results from multiple runs."""
+
+from __future__ import annotations
+
+import json
+import logging
+from dataclasses import dataclass
+from pathlib import Path
+
+logger = logging.getLogger(__name__)
+
+
+@dataclass
+class BenchmarkEntry:
+ """A single benchmark entry from results.json."""
+
+ command: str
+ mean: float
+ stddev: float | None
+ user: float
+ system: float
+ min: float
+ max: float
+ times: list[float]
+
+
+@dataclass
+class Comparison:
+ """Comparison of one entry against the baseline."""
+
+ name: str
+ mean: float
+ baseline_mean: float
+ speedup_percent: float
+ stddev: float | None
+
+
+@dataclass
+class CompareResult:
+ """Result of comparison."""
+
+ baseline: str
+ comparisons: list[Comparison]
+
+
+class ComparePhase:
+ """Compare benchmark results from multiple results.json files."""
+
+ def run(
+ self,
+ results_files: list[Path],
+ baseline: str | None = None,
+ ) -> CompareResult:
+ """Compare benchmark results.
+
+ Args:
+ results_files: List of results.json files to compare
+ baseline: Name of the baseline entry (default: first entry)
+
+ Returns:
+ CompareResult with comparison data
+ """
+ if not results_files:
+ raise ValueError("At least one results file is required")
+
+ # Load all entries from all files
+ all_entries: list[BenchmarkEntry] = []
+ for results_file in results_files:
+ if not results_file.exists():
+ raise FileNotFoundError(f"Results file not found: {results_file}")
+
+ logger.info(f"Loading results from: {results_file}")
+ with open(results_file) as f:
+ data = json.load(f)
+
+ entries = self._parse_results(data)
+ logger.info(f" Found {len(entries)} entries")
+ all_entries.extend(entries)
+
+ if not all_entries:
+ raise ValueError("No benchmark entries found in results files")
+
+ # Determine baseline
+ if baseline is None:
+ baseline = all_entries[0].command
+ logger.info(f"Using baseline: {baseline}")
+
+ # Find baseline entry
+ baseline_entry = None
+ for entry in all_entries:
+ if entry.command == baseline:
+ baseline_entry = entry
+ break
+
+ if baseline_entry is None:
+ available = [e.command for e in all_entries]
+ raise ValueError(
+ f"Baseline '{baseline}' not found. Available: {', '.join(available)}"
+ )
+
+ # Calculate comparisons
+ comparisons: list[Comparison] = []
+ for entry in all_entries:
+ if entry.command == baseline:
+ continue
+
+ speedup = self._calculate_speedup(baseline_entry.mean, entry.mean)
+ comparisons.append(
+ Comparison(
+ name=entry.command,
+ mean=entry.mean,
+ baseline_mean=baseline_entry.mean,
+ speedup_percent=speedup,
+ stddev=entry.stddev,
+ )
+ )
+
+ # Log results
+ logger.info("Comparison results:")
+ logger.info(f" Baseline ({baseline}): {baseline_entry.mean:.3f}s")
+ for comp in comparisons:
+ sign = "+" if comp.speedup_percent > 0 else ""
+ logger.info(
+ f" {comp.name}: {comp.mean:.3f}s ({sign}{comp.speedup_percent:.1f}%)"
+ )
+
+ return CompareResult(
+ baseline=baseline,
+ comparisons=comparisons,
+ )
+
+ def _parse_results(self, data: dict) -> list[BenchmarkEntry]:
+ """Parse results from hyperfine JSON output."""
+ entries = []
+
+ results = data.get("results", [])
+ for result in results:
+ entries.append(
+ BenchmarkEntry(
+ command=result.get("command", "unknown"),
+ mean=result.get("mean", 0),
+ stddev=result.get("stddev"),
+ user=result.get("user", 0),
+ system=result.get("system", 0),
+ min=result.get("min", 0),
+ max=result.get("max", 0),
+ times=result.get("times", []),
+ )
+ )
+
+ return entries
+
+ def _calculate_speedup(self, baseline_mean: float, other_mean: float) -> float:
+ """Calculate speedup percentage.
+
+ Positive = faster than baseline
+ Negative = slower than baseline
+ """
+ if baseline_mean == 0:
+ return 0.0
+ return round(((baseline_mean - other_mean) / baseline_mean) * 100, 1)
+
+ def to_json(self, result: CompareResult) -> str:
+ """Convert comparison result to JSON."""
+ return json.dumps(
+ {
+ "baseline": result.baseline,
+ "comparisons": [
+ {
+ "name": c.name,
+ "mean": c.mean,
+ "baseline_mean": c.baseline_mean,
+ "speedup_percent": c.speedup_percent,
+ "stddev": c.stddev,
+ }
+ for c in result.comparisons
+ ],
+ },
+ indent=2,
+ )
diff --git a/bench/config.py b/bench/config.py
new file mode 100644
index 000000000000..7991fee31bff
--- /dev/null
+++ b/bench/config.py
@@ -0,0 +1,231 @@
+"""Configuration management for benchcoin.
+
+Layered configuration (lowest to highest priority):
+1. Built-in defaults
+2. bench.toml config file
+3. Environment variables (BENCH_*)
+4. CLI arguments
+"""
+
+from __future__ import annotations
+
+import os
+import tomllib
+from dataclasses import dataclass
+from pathlib import Path
+from typing import Any
+
+
+# Built-in defaults
+DEFAULTS = {
+ "chain": "main",
+ "dbcache": 450,
+ "stop_height": 855000,
+ "runs": 3,
+ "connect": "", # Empty = use public P2P network
+ "binaries_dir": "./binaries",
+ "output_dir": "./bench-output",
+}
+
+# Profile overrides
+PROFILES = {
+ "quick": {
+ "stop_height": 1500,
+ "runs": 1,
+ },
+ "full": {
+ "stop_height": 855000,
+ "runs": 3,
+ },
+ "ci": {
+ "stop_height": 855000,
+ "runs": 3,
+ "connect": "148.251.128.115:33333",
+ },
+}
+
+# Environment variable mapping
+ENV_MAPPING = {
+ "BENCH_DATADIR": "datadir",
+ "BENCH_TMP_DATADIR": "tmp_datadir",
+ "BENCH_BINARIES_DIR": "binaries_dir",
+ "BENCH_OUTPUT_DIR": "output_dir",
+ "BENCH_STOP_HEIGHT": "stop_height",
+ "BENCH_DBCACHE": "dbcache",
+ "BENCH_CONNECT": "connect",
+ "BENCH_RUNS": "runs",
+ "BENCH_CHAIN": "chain",
+}
+
+
+@dataclass
+class Config:
+ """Benchmark configuration."""
+
+ # Core benchmark settings
+ chain: str = "main"
+ dbcache: int = 450
+ stop_height: int = 855000
+ runs: int = 3
+ connect: str = "" # Empty = use public P2P network
+
+ # Paths
+ datadir: str | None = None
+ tmp_datadir: str | None = None
+ binaries_dir: str = "./binaries"
+ output_dir: str = "./bench-output"
+
+ # Behavior flags
+ instrumented: bool = False
+ skip_existing: bool = False
+ no_cache_drop: bool = False
+ verbose: bool = False
+ dry_run: bool = False
+
+ # Profile used (for reference)
+ profile: str = "full"
+
+ def __post_init__(self) -> None:
+ # If tmp_datadir not set, derive from output_dir
+ if self.tmp_datadir is None:
+ self.tmp_datadir = str(Path(self.output_dir) / "tmp-datadir")
+
+ # Instrumented mode forces runs=1
+ if self.instrumented and self.runs != 1:
+ self.runs = 1
+
+ def validate(self) -> list[str]:
+ """Validate configuration, return list of errors."""
+ errors = []
+
+ if self.datadir is None:
+ errors.append("--datadir is required")
+ elif not Path(self.datadir).exists():
+ errors.append(f"datadir does not exist: {self.datadir}")
+
+ if self.stop_height < 1:
+ errors.append("stop_height must be positive")
+
+ if self.dbcache < 1:
+ errors.append("dbcache must be positive")
+
+ if self.runs < 1:
+ errors.append("runs must be positive")
+
+ if self.chain not in ("main", "testnet", "signet", "regtest"):
+ errors.append(f"invalid chain: {self.chain}")
+
+ return errors
+
+
+def load_toml(path: Path) -> tuple[dict[str, Any], dict[str, dict[str, Any]]]:
+ """Load configuration from TOML file.
+
+ Returns:
+ Tuple of (base_config, profiles_dict)
+ """
+ if not path.exists():
+ return {}, {}
+
+ with open(path, "rb") as f:
+ data = tomllib.load(f)
+
+ # Flatten structure: merge [defaults] and [paths] into top level
+ result = {}
+ if "defaults" in data:
+ result.update(data["defaults"])
+ if "paths" in data:
+ result.update(data["paths"])
+
+ # Extract profiles
+ profiles = data.get("profiles", {})
+
+ return result, profiles
+
+
+def load_env() -> dict[str, Any]:
+ """Load configuration from environment variables."""
+ result = {}
+
+ for env_var, config_key in ENV_MAPPING.items():
+ value = os.environ.get(env_var)
+ if value is not None:
+ # Convert numeric values
+ if config_key in ("stop_height", "dbcache", "runs"):
+ try:
+ value = int(value)
+ except ValueError:
+ pass # Keep as string, will fail validation
+ result[config_key] = value
+
+ return result
+
+
+def apply_profile(
+ config: dict[str, Any],
+ profile_name: str,
+ toml_profiles: dict[str, dict[str, Any]] | None = None,
+) -> dict[str, Any]:
+ """Apply a named profile to configuration.
+
+ Args:
+ config: Base configuration dict
+ profile_name: Name of profile to apply
+ toml_profiles: Profiles loaded from TOML file (override built-in)
+ """
+ result = config.copy()
+ result["profile"] = profile_name
+
+ # Apply built-in profile first
+ if profile_name in PROFILES:
+ result.update(PROFILES[profile_name])
+
+ # Then apply TOML profile (overrides built-in)
+ if toml_profiles and profile_name in toml_profiles:
+ result.update(toml_profiles[profile_name])
+
+ return result
+
+
+def build_config(
+ cli_args: dict[str, Any] | None = None,
+ config_file: Path | None = None,
+ profile: str = "full",
+) -> Config:
+ """Build configuration from all sources.
+
+ Priority (lowest to highest):
+ 1. Built-in defaults
+ 2. Config file (bench.toml) base settings
+ 3. Built-in profile overrides
+ 4. Config file profile overrides
+ 5. Environment variables
+ 6. CLI arguments
+ """
+ # Start with defaults
+ config = DEFAULTS.copy()
+
+ # Load config file
+ if config_file is None:
+ config_file = Path("bench.toml")
+ file_config, toml_profiles = load_toml(config_file)
+ config.update(file_config)
+
+ # Apply profile (built-in first, then TOML overrides)
+ config = apply_profile(config, profile, toml_profiles)
+
+ # Load environment variables
+ env_config = load_env()
+ config.update(env_config)
+
+ # Apply CLI arguments (filter out None values)
+ if cli_args:
+ for key, value in cli_args.items():
+ if value is not None:
+ config[key] = value
+
+ # Build Config object (filter to only valid fields)
+ valid_fields = {f.name for f in Config.__dataclass_fields__.values()}
+ filtered = {k: v for k, v in config.items() if k in valid_fields}
+
+ return Config(**filtered)
diff --git a/bench/patchelf.py b/bench/patchelf.py
new file mode 100644
index 000000000000..6da1e00867cf
--- /dev/null
+++ b/bench/patchelf.py
@@ -0,0 +1,135 @@
+"""Patchelf utilities for fixing guix-built binaries on NixOS."""
+
+from __future__ import annotations
+
+import logging
+import os
+import subprocess
+from pathlib import Path
+
+logger = logging.getLogger(__name__)
+
+
+def get_nix_interpreter() -> str | None:
+ """Get the path to the nix store's dynamic linker.
+
+ Returns None if not on NixOS or can't find it.
+ """
+ # Check if we're on NixOS
+ if not Path("/etc/NIXOS").exists():
+ return None
+
+ # Find the interpreter from the current glibc
+ # We can get this by checking what the current shell uses
+ try:
+ result = subprocess.run(
+ ["patchelf", "--print-interpreter", "/bin/sh"],
+ capture_output=True,
+ text=True,
+ )
+ if result.returncode == 0:
+ interp = result.stdout.strip()
+ if interp and Path(interp).exists():
+ return interp
+ except FileNotFoundError:
+ pass
+
+ return None
+
+
+def get_binary_interpreter(binary: Path) -> str | None:
+ """Get the interpreter (dynamic linker) of a binary."""
+ try:
+ result = subprocess.run(
+ ["patchelf", "--print-interpreter", str(binary)],
+ capture_output=True,
+ text=True,
+ )
+ if result.returncode == 0:
+ return result.stdout.strip()
+ except FileNotFoundError:
+ logger.debug("patchelf not found")
+ return None
+
+
+def needs_patching(binary: Path) -> bool:
+ """Check if a binary needs to be patched for NixOS.
+
+ Returns True if:
+ - We're on NixOS
+ - The binary has a non-nix interpreter (e.g., /lib64/ld-linux-x86-64.so.2)
+ """
+ nix_interp = get_nix_interpreter()
+ if not nix_interp:
+ # Not on NixOS, no patching needed
+ return False
+
+ binary_interp = get_binary_interpreter(binary)
+ if not binary_interp:
+ # Can't determine interpreter, assume no patching needed
+ return False
+
+ # Check if the binary's interpreter is already in the nix store
+ if binary_interp.startswith("/nix/store/"):
+ return False
+
+ # Binary uses a non-nix interpreter (e.g., /lib64/...)
+ return True
+
+
+def patch_binary(binary: Path) -> bool:
+ """Patch a binary to use the nix store's dynamic linker.
+
+ Returns True if patching was successful or not needed.
+ """
+ if not needs_patching(binary):
+ logger.debug(f"Binary {binary} does not need patching")
+ return True
+
+ nix_interp = get_nix_interpreter()
+ if not nix_interp:
+ logger.warning("Cannot patch binary: unable to find nix interpreter")
+ return False
+
+ original_interp = get_binary_interpreter(binary)
+ logger.info(f"Patching binary: {binary}")
+ logger.info(f" Original interpreter: {original_interp}")
+ logger.info(f" New interpreter: {nix_interp}")
+
+ # Make sure binary is writable
+ try:
+ os.chmod(binary, 0o755)
+ except OSError as e:
+ logger.warning(f"Could not make binary writable: {e}")
+
+ try:
+ result = subprocess.run(
+ ["patchelf", "--set-interpreter", nix_interp, str(binary)],
+ capture_output=True,
+ text=True,
+ )
+ if result.returncode != 0:
+ logger.error(f"patchelf failed: {result.stderr}")
+ return False
+ logger.info(" Patching successful")
+ return True
+ except FileNotFoundError:
+ logger.error("patchelf not found - install it or use nix develop")
+ return False
+
+
+def ensure_binary_runnable(binary: Path) -> bool:
+ """Ensure a binary can run on this system.
+
+ Patches the binary if necessary (on NixOS with non-nix binaries).
+ Returns True if the binary should be runnable.
+ """
+ if not binary.exists():
+ logger.error(f"Binary not found: {binary}")
+ return False
+
+ # Check if patching is needed and do it
+ if needs_patching(binary):
+ return patch_binary(binary)
+
+ return True
diff --git a/bench/report.py b/bench/report.py
new file mode 100644
index 000000000000..41562a9a6158
--- /dev/null
+++ b/bench/report.py
@@ -0,0 +1,668 @@
+"""Report phase - generate HTML reports from benchmark results.
+
+Ported from the JavaScript logic in .github/workflows/publish-results.yml.
+"""
+
+from __future__ import annotations
+
+import json
+import logging
+import re
+import shutil
+from dataclasses import dataclass, field
+from pathlib import Path
+from typing import Any
+
+logger = logging.getLogger(__name__)
+
+# HTML template for individual run report
+RUN_REPORT_TEMPLATE = """
+
+
+ Benchmark Results
+
+
+
+
+
Benchmark Results
+
+
{title}
+
+
+
Run Data
+
+
+
+
+ Network
+ Command
+ Mean (s)
+ Std Dev
+ User (s)
+ System (s)
+
+
+
+ {run_data_rows}
+
+
+
+
+
+
Speedup Summary
+
+
+
+
+ Network
+ Speedup (%)
+
+
+
+ {speedup_rows}
+
+
+
+
+
+ {graphs_section}
+
+
+
+"""
+
+# HTML template for main index
+INDEX_TEMPLATE = """
+
+
+ Bitcoin Benchmark Results
+
+
+
+
+
Bitcoin Benchmark Results
+
+
+
+"""
+
+
+@dataclass
+class BenchmarkRun:
+ """Parsed benchmark run data."""
+
+ network: str
+ command: str
+ mean: float
+ stddev: float | None
+ user: float
+ system: float
+ parameters: dict[str, Any] = field(default_factory=dict)
+
+
+@dataclass
+class ReportResult:
+ """Result of report generation."""
+
+ output_dir: Path
+ index_file: Path
+ speedups: dict[str, float]
+
+
+class ReportGenerator:
+ """Generate HTML reports from benchmark results."""
+
+ def __init__(
+ self, repo_url: str = "https://github.com/bitcoin-dev-tools/benchcoin"
+ ):
+ self.repo_url = repo_url
+
+ def generate_multi_network(
+ self,
+ network_dirs: dict[str, Path],
+ output_dir: Path,
+ title: str = "Benchmark Results",
+ pr_number: str | None = None,
+ run_id: str | None = None,
+ ) -> ReportResult:
+ """Generate HTML report from multiple network benchmark results.
+
+ Args:
+ network_dirs: Dict mapping network name to directory containing results.json
+ output_dir: Where to write the HTML report
+ title: Title for the report
+ pr_number: PR number (for CI reports)
+ run_id: Run ID (for CI reports)
+
+ Returns:
+ ReportResult with paths and speedup data
+ """
+ output_dir.mkdir(parents=True, exist_ok=True)
+
+ # Combine results from all networks
+ all_runs: list[BenchmarkRun] = []
+ for network, input_dir in network_dirs.items():
+ results_file = input_dir / "results.json"
+ if not results_file.exists():
+ logger.warning(
+ f"results.json not found in {input_dir} for network {network}"
+ )
+ continue
+
+ with open(results_file) as f:
+ data = json.load(f)
+
+ # Parse and add network to each run
+ for result in data.get("results", []):
+ all_runs.append(
+ BenchmarkRun(
+ network=network,
+ command=result.get("command", ""),
+ mean=result.get("mean", 0),
+ stddev=result.get("stddev"),
+ user=result.get("user", 0),
+ system=result.get("system", 0),
+ parameters=result.get("parameters", {}),
+ )
+ )
+
+ # Copy artifacts from this network
+ self._copy_network_artifacts(network, input_dir, output_dir)
+
+ if not all_runs:
+ raise ValueError("No benchmark results found in any network directory")
+
+ # Calculate speedups per network
+ speedups = self._calculate_speedups_per_network(all_runs)
+
+ # Build title with PR/run info if provided
+ full_title = title
+ if pr_number and run_id:
+ full_title = f"PR #{pr_number} - Run {run_id}"
+
+ # Generate HTML
+ html = self._generate_html(
+ all_runs, speedups, full_title, output_dir, output_dir
+ )
+
+ # Write report
+ index_file = output_dir / "index.html"
+ index_file.write_text(html)
+ logger.info(f"Generated report: {index_file}")
+
+ # Write combined results.json
+ combined_results = {
+ "results": [
+ {
+ "network": run.network,
+ "command": run.command,
+ "mean": run.mean,
+ "stddev": run.stddev,
+ "user": run.user,
+ "system": run.system,
+ }
+ for run in all_runs
+ ],
+ "speedups": speedups,
+ }
+ results_file = output_dir / "results.json"
+ results_file.write_text(json.dumps(combined_results, indent=2))
+
+ return ReportResult(
+ output_dir=output_dir,
+ index_file=index_file,
+ speedups=speedups,
+ )
+
+ def generate(
+ self,
+ input_dir: Path,
+ output_dir: Path,
+ title: str = "Benchmark Results",
+ ) -> ReportResult:
+ """Generate HTML report from benchmark artifacts.
+
+ Args:
+ input_dir: Directory containing results.json and artifacts
+ output_dir: Where to write the HTML report
+ title: Title for the report
+
+ Returns:
+ ReportResult with paths and speedup data
+ """
+ output_dir.mkdir(parents=True, exist_ok=True)
+
+ # Load results.json
+ results_file = input_dir / "results.json"
+ if not results_file.exists():
+ raise FileNotFoundError(f"results.json not found in {input_dir}")
+
+ with open(results_file) as f:
+ data = json.load(f)
+
+ # Parse results
+ runs = self._parse_results(data)
+
+ # Calculate speedups
+ speedups = self._calculate_speedups(runs)
+
+ # Generate HTML
+ html = self._generate_html(runs, speedups, title, input_dir, output_dir)
+
+ # Write report
+ index_file = output_dir / "index.html"
+ index_file.write_text(html)
+ logger.info(f"Generated report: {index_file}")
+
+ # Copy artifacts (flamegraphs, plots)
+ self._copy_artifacts(input_dir, output_dir)
+
+ return ReportResult(
+ output_dir=output_dir,
+ index_file=index_file,
+ speedups=speedups,
+ )
+
+ def generate_index(
+ self,
+ results_dir: Path,
+ output_file: Path,
+ ) -> None:
+ """Generate main index.html listing all available results.
+
+ Args:
+ results_dir: Directory containing pr-* subdirectories
+ output_file: Where to write index.html
+ """
+ runs = []
+
+ if results_dir.exists():
+ for pr_dir in sorted(results_dir.iterdir()):
+ if pr_dir.is_dir() and pr_dir.name.startswith("pr-"):
+ pr_num = pr_dir.name.replace("pr-", "")
+ pr_runs = []
+ for run_dir in sorted(pr_dir.iterdir()):
+ if run_dir.is_dir():
+ pr_runs.append(run_dir.name)
+ if pr_runs:
+ runs.append((pr_num, pr_runs))
+
+ run_list_html = ""
+ for pr_num, pr_runs in runs:
+ run_links = "\n".join(
+ f'Run {run} '
+ for run in pr_runs
+ )
+ run_list_html += f"""
+ PR #{pr_num}
+
+
+ """
+
+ html = INDEX_TEMPLATE.format(run_list=run_list_html)
+ output_file.write_text(html)
+ logger.info(f"Generated index: {output_file}")
+
+ def _parse_results(self, data: dict) -> list[BenchmarkRun]:
+ """Parse results from hyperfine JSON output."""
+ runs = []
+
+ # Handle both direct hyperfine output and combined results format
+ results = data.get("results", [])
+
+ for result in results:
+ runs.append(
+ BenchmarkRun(
+ network=result.get("network", "default"),
+ command=result.get("command", ""),
+ mean=result.get("mean", 0),
+ stddev=result.get("stddev"),
+ user=result.get("user", 0),
+ system=result.get("system", 0),
+ parameters=result.get("parameters", {}),
+ )
+ )
+
+ return runs
+
+ def _calculate_speedups(self, runs: list[BenchmarkRun]) -> dict[str, float]:
+ """Calculate speedup percentages.
+
+ Uses the first entry as baseline and compares all others against it.
+ Returns a dict mapping command name to speedup percentage.
+ """
+ speedups = {}
+
+ if len(runs) < 2:
+ return speedups
+
+ # Use first run as baseline
+ baseline = runs[0]
+ baseline_mean = baseline.mean
+
+ if baseline_mean <= 0:
+ return speedups
+
+ # Calculate speedup for each other run
+ for run in runs[1:]:
+ speedup = ((baseline_mean - run.mean) / baseline_mean) * 100
+ # Use command name as key, extracting just the name part
+ name = run.command
+ speedups[name] = round(speedup, 1)
+
+ return speedups
+
+ def _calculate_speedups_per_network(
+ self, runs: list[BenchmarkRun]
+ ) -> dict[str, float]:
+ """Calculate speedup percentages per network.
+
+ For each network, uses 'base' as baseline and calculates speedup for 'head'.
+ Returns a dict mapping network name to speedup percentage.
+ """
+ speedups = {}
+
+ # Group runs by network
+ networks: dict[str, list[BenchmarkRun]] = {}
+ for run in runs:
+ if run.network not in networks:
+ networks[run.network] = []
+ networks[run.network].append(run)
+
+ # Calculate speedup for each network
+ for network, network_runs in networks.items():
+ base_mean = None
+ head_mean = None
+
+ for run in network_runs:
+ if run.command == "base":
+ base_mean = run.mean
+ elif run.command == "head":
+ head_mean = run.mean
+
+ if base_mean and head_mean and base_mean > 0:
+ speedup = ((base_mean - head_mean) / base_mean) * 100
+ speedups[network] = round(speedup, 1)
+
+ return speedups
+
+ def _copy_network_artifacts(
+ self, network: str, input_dir: Path, output_dir: Path
+ ) -> None:
+ """Copy artifacts from a network directory with network prefix."""
+ # Copy flamegraphs with network prefix
+ for svg in input_dir.glob("*-flamegraph.svg"):
+ dest = output_dir / f"{network}-{svg.name}"
+ shutil.copy2(svg, dest)
+ logger.debug(f"Copied {svg.name} as {dest.name}")
+
+ # Copy plots directory with network prefix
+ plots_dir = input_dir / "plots"
+ if plots_dir.exists():
+ dest_plots = output_dir / f"{network}-plots"
+ if dest_plots.exists():
+ shutil.rmtree(dest_plots)
+ shutil.copytree(plots_dir, dest_plots)
+ logger.debug(f"Copied plots to {dest_plots}")
+
+ def _generate_html(
+ self,
+ runs: list[BenchmarkRun],
+ speedups: dict[str, float],
+ title: str,
+ input_dir: Path,
+ output_dir: Path,
+ ) -> str:
+ """Generate the HTML report."""
+ # Sort runs by network then by command (base first)
+ sorted_runs = sorted(
+ runs,
+ key=lambda r: (r.network, 0 if "base" in r.command.lower() else 1),
+ )
+
+ # Generate run data rows
+ run_data_rows = ""
+ for run in sorted_runs:
+ # Create commit link if there's a commit hash in the command
+ command_html = self._linkify_commit(run.command)
+
+ stddev_str = f"{run.stddev:.3f}" if run.stddev else "N/A"
+
+ run_data_rows += f"""
+
+ {run.network}
+ {command_html}
+ {run.mean:.3f}
+ {stddev_str}
+ {run.user:.3f}
+ {run.system:.3f}
+
+ """
+
+ # Generate speedup rows
+ speedup_rows = ""
+ if sorted_runs:
+ # Add baseline row
+ baseline = sorted_runs[0]
+ speedup_rows += f"""
+
+ {baseline.command} (baseline)
+ -
+
+ """
+ for name, speedup in speedups.items():
+ # Skip instrumented runs in speedup summary
+ if name.lower().endswith("-instrumented"):
+ continue
+
+ color_class = ""
+ if speedup > 0:
+ color_class = "text-green-600"
+ elif speedup < 0:
+ color_class = "text-red-600"
+
+ sign = "+" if speedup > 0 else ""
+ speedup_rows += f"""
+
+ {name}
+ {sign}{speedup}%
+
+ """
+
+ # Generate graphs section
+ graphs_section = self._generate_graphs_section(runs, input_dir, output_dir)
+
+ return RUN_REPORT_TEMPLATE.format(
+ title=title,
+ run_data_rows=run_data_rows,
+ speedup_rows=speedup_rows,
+ graphs_section=graphs_section,
+ )
+
+ def _linkify_commit(self, command: str) -> str:
+ """Convert commit hashes in command to links."""
+
+ def replace_commit(match):
+ commit = match.group(1)
+ short_commit = commit[:8] if len(commit) > 8 else commit
+ return f'({short_commit} )'
+
+ return re.sub(r"\(([a-f0-9]{7,40})\)", replace_commit, command)
+
+ def _generate_graphs_section(
+ self,
+ runs: list[BenchmarkRun],
+ input_dir: Path,
+ output_dir: Path,
+ ) -> str:
+ """Generate the flamegraphs and plots section."""
+ graphs_html = ""
+
+ for run in runs:
+ # Use the command/name directly (e.g., "base", "head")
+ name = run.command
+ network = run.network
+
+ # Check for flamegraph - try both with and without network prefix
+ # Network-prefixed: {network}-{name}-flamegraph.svg (for multi-network reports)
+ # Non-prefixed: {name}-flamegraph.svg (for single-network reports)
+ flamegraph_name = None
+ flamegraph_path = None
+
+ network_prefixed = f"{network}-{name}-flamegraph.svg"
+ non_prefixed = f"{name}-flamegraph.svg"
+
+ if (output_dir / network_prefixed).exists():
+ flamegraph_name = network_prefixed
+ flamegraph_path = output_dir / network_prefixed
+ elif (input_dir / non_prefixed).exists():
+ flamegraph_name = non_prefixed
+ flamegraph_path = input_dir / non_prefixed
+
+ # Check for plots - try both network-prefixed and non-prefixed directories
+ plot_files = []
+ plots_dir = None
+
+ network_plots_dir = output_dir / f"{network}-plots"
+ regular_plots_dir = input_dir / "plots"
+
+ if network_plots_dir.exists():
+ plots_dir = network_plots_dir
+ plot_files = [
+ p.name
+ for p in plots_dir.iterdir()
+ if p.name.startswith(f"{name}-") and p.suffix == ".png"
+ ]
+ elif regular_plots_dir.exists():
+ plots_dir = regular_plots_dir
+ plot_files = [
+ p.name
+ for p in plots_dir.iterdir()
+ if p.name.startswith(f"{name}-") and p.suffix == ".png"
+ ]
+
+ if not flamegraph_path and not plot_files:
+ continue
+
+ # Build display label
+ display_label = f"{network} - {name}" if network != "default" else name
+
+ graphs_html += f"""
+
+
{display_label}
+ """
+
+ if flamegraph_path:
+ graphs_html += f"""
+
+ """
+
+ if plot_files and plots_dir:
+ # Determine the relative path for plots
+ plots_rel_path = plots_dir.name
+ for plot in sorted(plot_files):
+ graphs_html += f"""
+
+
+
+ """
+
+ graphs_html += "
"
+
+ if graphs_html:
+ return f"""
+ Flamegraphs and Plots
+ {graphs_html}
+ """
+
+ return ""
+
+ def _copy_artifacts(self, input_dir: Path, output_dir: Path) -> None:
+ """Copy flamegraphs and plots to output directory."""
+ # Skip if input and output are the same directory
+ if input_dir.resolve() == output_dir.resolve():
+ logger.debug("Input and output are the same directory, skipping copy")
+ return
+
+ # Copy flamegraphs
+ for svg in input_dir.glob("*-flamegraph.svg"):
+ dest = output_dir / svg.name
+ shutil.copy2(svg, dest)
+ logger.debug(f"Copied {svg.name}")
+
+ # Copy plots directory
+ plots_dir = input_dir / "plots"
+ if plots_dir.exists():
+ dest_plots = output_dir / "plots"
+ if dest_plots.exists():
+ shutil.rmtree(dest_plots)
+ shutil.copytree(plots_dir, dest_plots)
+ logger.debug("Copied plots directory")
+
+
+class ReportPhase:
+ """Generate reports from benchmark results."""
+
+ def __init__(
+ self, repo_url: str = "https://github.com/bitcoin-dev-tools/benchcoin"
+ ):
+ self.generator = ReportGenerator(repo_url)
+
+ def run(
+ self,
+ input_dir: Path,
+ output_dir: Path,
+ title: str = "Benchmark Results",
+ ) -> ReportResult:
+ """Generate report from benchmark artifacts.
+
+ Args:
+ input_dir: Directory containing results.json and artifacts
+ output_dir: Where to write the HTML report
+ title: Title for the report
+
+ Returns:
+ ReportResult with paths and speedup data
+ """
+ return self.generator.generate(input_dir, output_dir, title)
+
+ def run_multi_network(
+ self,
+ network_dirs: dict[str, Path],
+ output_dir: Path,
+ title: str = "Benchmark Results",
+ pr_number: str | None = None,
+ run_id: str | None = None,
+ ) -> ReportResult:
+ """Generate report from multiple network benchmark results.
+
+ Args:
+ network_dirs: Dict mapping network name to directory containing results.json
+ output_dir: Where to write the HTML report
+ title: Title for the report
+ pr_number: PR number (for CI reports)
+ run_id: Run ID (for CI reports)
+
+ Returns:
+ ReportResult with paths and speedup data
+ """
+ return self.generator.generate_multi_network(
+ network_dirs, output_dir, title, pr_number, run_id
+ )
+
+ def update_index(self, results_dir: Path, output_file: Path) -> None:
+ """Update the main index.html listing all results.
+
+ Args:
+ results_dir: Directory containing pr-* subdirectories
+ output_file: Where to write index.html
+ """
+ self.generator.generate_index(results_dir, output_file)
diff --git a/bench/utils.py b/bench/utils.py
new file mode 100644
index 000000000000..df454cf0644e
--- /dev/null
+++ b/bench/utils.py
@@ -0,0 +1,105 @@
+"""Utility functions for git operations."""
+
+from __future__ import annotations
+
+import logging
+import subprocess
+from pathlib import Path
+
+logger = logging.getLogger(__name__)
+
+
+class GitState:
+ """Saved git state for restoration after operations."""
+
+ def __init__(self, repo_path: Path | None = None):
+ self.repo_path = repo_path or Path.cwd()
+ self.original_branch: str | None = None
+ self.original_commit: str | None = None
+ self.was_detached: bool = False
+
+ def save(self) -> None:
+ """Save current git state."""
+ # Check if we're on a branch or detached HEAD
+ result = subprocess.run(
+ ["git", "symbolic-ref", "--short", "HEAD"],
+ capture_output=True,
+ text=True,
+ cwd=self.repo_path,
+ )
+
+ if result.returncode == 0:
+ self.original_branch = result.stdout.strip()
+ self.was_detached = False
+ else:
+ # Detached HEAD - save commit hash
+ result = subprocess.run(
+ ["git", "rev-parse", "HEAD"],
+ capture_output=True,
+ text=True,
+ check=True,
+ cwd=self.repo_path,
+ )
+ self.original_commit = result.stdout.strip()
+ self.was_detached = True
+
+ logger.debug(
+ f"Saved git state: branch={self.original_branch}, "
+ f"commit={self.original_commit}, detached={self.was_detached}"
+ )
+
+ def restore(self) -> None:
+ """Restore saved git state."""
+ if self.original_branch:
+ logger.debug(f"Restoring branch: {self.original_branch}")
+ subprocess.run(
+ ["git", "checkout", self.original_branch],
+ check=True,
+ cwd=self.repo_path,
+ )
+ elif self.original_commit:
+ logger.debug(f"Restoring detached HEAD: {self.original_commit}")
+ subprocess.run(
+ ["git", "checkout", self.original_commit],
+ check=True,
+ cwd=self.repo_path,
+ )
+
+
+class GitError(Exception):
+ """Git operation failed."""
+
+ pass
+
+
+def git_checkout(commit: str, repo_path: Path | None = None) -> None:
+ """Checkout a specific commit."""
+ repo_path = repo_path or Path.cwd()
+ logger.info(f"Checking out {commit[:12]}")
+
+ result = subprocess.run(
+ ["git", "checkout", commit],
+ cwd=repo_path,
+ capture_output=True,
+ text=True,
+ )
+
+ if result.returncode != 0:
+ raise GitError(f"Failed to checkout {commit}: {result.stderr}")
+
+
+def git_rev_parse(ref: str, repo_path: Path | None = None) -> str:
+ """Resolve a git reference to a full commit hash."""
+ repo_path = repo_path or Path.cwd()
+
+ result = subprocess.run(
+ ["git", "rev-parse", ref],
+ cwd=repo_path,
+ capture_output=True,
+ text=True,
+ )
+
+ if result.returncode != 0:
+ raise GitError(f"Failed to resolve {ref}: {result.stderr}")
+
+ return result.stdout.strip()
diff --git a/doc/benchcoin.md b/doc/benchcoin.md
new file mode 100644
index 000000000000..0b4159256c95
--- /dev/null
+++ b/doc/benchcoin.md
@@ -0,0 +1,127 @@
+# benchcoin
+
+A Bitcoin Core benchmarking fork
+
+This repository is a fork of Bitcoin Core that performs automated IBD benchmarking.
+It allows you to measure and compare the performance impact of certain types of changes to Bitcoin Core's codebase on a longer-running IBD benchmark, in a (pretty) reproducible fashion.
+
+## Features
+
+- Automated IBD benchmarking on pull requests
+- Multiple configurations:
+ - Mainnet with default cache
+ - Mainnet with large cache
+- Performance visualizations including:
+ - Flamegraphs for CPU profiling
+ - Time series plots of various metrics
+ - Compare `base` (bitcoin/bitcoin:master) and `head` (PR)
+
+## Example Flamegraph
+
+Below is an example flamegraph showing CPU utilization during IBD:
+
+
+
+## How to use it
+
+1. Open a Pull Request against **this repo**
+2. Wait for the bot to comment on your PR after it's finished.
+
+See the [Contributing](#contributing) section for more details.
+
+## How it works
+
+When you open a pull request against this repository:
+
+1. The CI workflow automatically builds both the base and PR versions of bitcoind
+2. Runs IBD benchmarks
+3. Records performance metrics and creates various visualizations
+4. Posts results as a comment on your PR
+
+The benchmarks test three configurations:
+- Mainnet-default: with default (450 MB) dbcache
+ - From a pruned datadir @ height 840,000 to height 855,000
+- Mainnet-large: with 32000 MB dbcache
+ - From a pruned datadir @ height 840,000 to height 855,000
+
+## Benchmark Outputs
+
+For each benchmark run, you'll get a github pages page with:
+
+- Timing comparisons between base and PR versions
+- CPU flamegraphs showing where time is spent
+- Time series plots showing:
+ - Block height vs time
+ - Cache size vs block height
+ - Cache size vs time
+ - Transaction count vs block height
+ - Coins cache size vs time
+ - LevelDB metrics
+ - Memory pool metrics
+
+## Local Development (WIP)
+
+To run benchmarks locally (WIP, and Linux-only due to [shell.nix](../shell.nix) limitations):
+
+1. Make sure you have [Nix package manager](https://nixos.org/download/) installed
+
+2. Setup the Nix development environment:
+```bash
+nix-shell
+```
+
+3. Run a local benchmark:
+```bash
+just run-signet
+```
+
+This will:
+- Create a temporary directory for testing
+- Build both base and PR versions
+- Download the required UTXO snapshot if needed
+- Run the benchmark
+- Generate performance visualizations
+
+## Technical Details
+
+The benchmarking system uses:
+- [Hyperfine](https://github.com/sharkdp/hyperfine) for benchmark timing
+- [Flamegraph](https://github.com/willcl-ark/flamegraph) for CPU profiling
+- [matplotlib](https://matplotlib.org/) for metric visualization
+- [GitHub Actions](https://github.com/features/actions) for CI automation
+
+The system copies over a pruned datadir to speed up IBD to a more interesting height (840k).
+
+### Runner & seed
+
+The CI runner is self-hosted on a Hetzner AX52 running at the bitcoin-dev-tools organsation level.
+It is running NixOS using configuration found in this repo: [nix-github-runner](https://github.com/bitcoin-dev-tools/nix-github-runner) for easier deployment and reproducibility.
+
+The runner host has 16 cores, with one used for system, one for `flamegraph` (i.e. `perf record`) and 14 dedicated to the Bitcoin Core node under test.
+
+The benchmarking peer on the runner is served blocks over the (real) "internet" (it may be LAN as it's within a single Hetzner region) via a single peer to exercise full IBD codepaths. This naturally may introduce some variance, but it was deemed preferable to running another bitcoin core on the same machine.
+
+This seed peer is another Hetzner VPS in the same region, and its configuration can be found here: [nix-seed-node](https://github.com/bitcoin-dev-tools/nix-seed-node)
+
+## Contributing
+
+### Benchmark an existing bitcoin/bitcoin PR
+
+This requires `just` be installed. If you don't have `just` installed you can run the commands in the [justfile](../justfile) manually.
+
+1. Fork this repository (or bitcoin/bitcoin and add this as a remote)
+2. Create a new branch from benchcoin/master
+3. Run: `just pick-pr ` to cherry-pick commits from the PR
+4. Push the branch
+5. Open a pull request **against this repo. NOT bitcoin/bitcoin**
+
+### Benchmark standalone/new changes
+
+1. Fork this repository (or bitcoin/bitcoin and add this as a remote)
+2. Make your changes to Bitcoin Core
+3. Open a pull request **against this repo. NOT bitcoin/bitcoin**
+4. Wait for benchmark results to be posted on your PR here
+
+## License
+
+This project is licensed under the same terms as Bitcoin Core - see the [COPYING](../COPYING) file for details.
diff --git a/doc/flamegraph.svg b/doc/flamegraph.svg
new file mode 100644
index 000000000000..77f05068edd1
--- /dev/null
+++ b/doc/flamegraph.svg
@@ -0,0 +1,491 @@
+bitcoind assumeutxo IBD@head Reset Zoom Search [unknown] (930,216,305 samples, 0.03%) libc.so.6::__GI___libc_open (1,277,437,934 samples, 0.04%) [unknown] (1,277,437,934 samples, 0.04%) [unknown] (1,121,698,471 samples, 0.03%) [unknown] (1,121,698,471 samples, 0.03%) [unknown] (1,121,698,471 samples, 0.03%) [unknown] (808,723,138 samples, 0.02%) [unknown] (705,370,773 samples, 0.02%) [unknown] (654,247,113 samples, 0.02%) [unknown] (601,840,190 samples, 0.02%) [unknown] (412,286,776 samples, 0.01%) libc.so.6::__lll_lock_wait_private (3,169,140,832 samples, 0.09%) [unknown] (3,068,852,192 samples, 0.09%) [unknown] (2,912,247,498 samples, 0.08%) [unknown] (2,859,869,350 samples, 0.08%) [unknown] (2,547,374,665 samples, 0.07%) [unknown] (2,442,338,234 samples, 0.07%) [unknown] (2,018,530,007 samples, 0.06%) [unknown] (1,768,059,272 samples, 0.05%) [unknown] (1,360,516,543 samples, 0.04%) [unknown] (941,780,033 samples, 0.03%) [unknown] (732,126,125 samples, 0.02%) [unknown] (367,091,733 samples, 0.01%) libc.so.6::__lll_lock_wake_private (53,149,822,463 samples, 1.49%) l.. [unknown] (52,891,684,033 samples, 1.49%) [.. [unknown] (51,489,363,011 samples, 1.45%) [.. [unknown] (51,020,482,662 samples, 1.43%) [.. [unknown] (46,915,115,303 samples, 1.32%) [unknown] (45,255,852,290 samples, 1.27%) [unknown] (38,150,418,340 samples, 1.07%) [unknown] (35,292,486,865 samples, 0.99%) [unknown] (7,892,404,247 samples, 0.22%) [unknown] (3,327,749,547 samples, 0.09%) [unknown] (1,188,855,625 samples, 0.03%) [unknown] (566,758,595 samples, 0.02%) libc.so.6::_int_free_create_chunk (628,326,946 samples, 0.02%) libc.so.6::_int_free_merge_chunk (358,656,602 samples, 0.01%) libc.so.6::_int_malloc (74,559,659,927 samples, 2.10%) li.. [unknown] (721,620,417 samples, 0.02%) [unknown] (610,988,583 samples, 0.02%) [unknown] (610,988,583 samples, 0.02%) [unknown] (610,988,583 samples, 0.02%) [unknown] (559,250,914 samples, 0.02%) [unknown] (559,250,914 samples, 0.02%) libc.so.6::alloc_perturb (425,154,213 samples, 0.01%) libc.so.6::malloc (24,700,554,078 samples, 0.69%) libc.so.6::malloc_consolidate (735,996,757 samples, 0.02%) libc.so.6::unlink_chunk.isra.0 (6,120,352,373 samples, 0.17%) [unknown] (167,607,884,597 samples, 4.71%) [unknown] libstdc++.so.6.0.32::virtual thunk to std::__cxx11::basic_ostringstream<char, std::char_traits<char>, std::allocator<char> >::~basic_ostringstream (417,178,495 samples, 0.01%) [unknown] (417,178,495 samples, 0.01%) libc.so.6::_IO_default_xsputn (371,898,668 samples, 0.01%) libc.so.6::_IO_do_write@@GLIBC_2.2.5 (415,186,042 samples, 0.01%) libc.so.6::_IO_file_xsputn@@GLIBC_2.2.5 (52,841,892,362 samples, 1.49%) l.. libc.so.6::_IO_fwrite (157,971,658,633 samples, 4.44%) libc.so... [[ext4]] (1,657,432,113 samples, 0.05%) [unknown] (573,069,492 samples, 0.02%) [[ext4]] (2,536,153,731 samples, 0.07%) [[ext4]] (10,537,322,599 samples, 0.30%) [unknown] (7,422,408,080 samples, 0.21%) [unknown] (6,329,696,449 samples, 0.18%) [unknown] (5,353,636,150 samples, 0.15%) [unknown] (5,041,980,997 samples, 0.14%) [unknown] (3,383,888,214 samples, 0.10%) [unknown] (1,348,486,405 samples, 0.04%) [unknown] (477,579,410 samples, 0.01%) [unknown] (424,961,857 samples, 0.01%) [[ext4]] (48,707,811,335 samples, 1.37%) [.. [unknown] (37,296,429,178 samples, 1.05%) [unknown] (35,118,068,672 samples, 0.99%) [unknown] (29,610,843,695 samples, 0.83%) [unknown] (24,208,827,110 samples, 0.68%) [unknown] (17,096,181,771 samples, 0.48%) [unknown] (6,112,761,166 samples, 0.17%) [unknown] (1,344,893,459 samples, 0.04%) [unknown] (458,831,632 samples, 0.01%) [[ext4]] (365,017,200 samples, 0.01%) [[ext4]] (518,180,627 samples, 0.01%) [[ext4]] (466,259,788 samples, 0.01%) [[ext4]] (673,383,386 samples, 0.02%) [[ext4]] (59,764,846,104 samples, 1.68%) [.. [unknown] (58,060,722,922 samples, 1.63%) [.. [unknown] (7,950,480,723 samples, 0.22%) [unknown] (5,540,377,500 samples, 0.16%) [unknown] (865,590,582 samples, 0.02%) [unknown] (813,212,612 samples, 0.02%) [unknown] (813,212,612 samples, 0.02%) [unknown] (813,212,612 samples, 0.02%) [unknown] (711,368,524 samples, 0.02%) libc.so.6::__GI___libc_write (70,786,161,691 samples, 1.99%) li.. [unknown] (70,568,950,557 samples, 1.98%) [u.. [unknown] (69,379,113,892 samples, 1.95%) [u.. [unknown] (68,772,280,665 samples, 1.93%) [u.. [unknown] (66,697,097,059 samples, 1.88%) [u.. [unknown] (3,800,961,354 samples, 0.11%) [unknown] (780,895,718 samples, 0.02%) libc.so.6::__memmove_avx512_unaligned_erms (15,769,232,267 samples, 0.44%) libc.so.6::__mempcpy@plt (4,938,637,189 samples, 0.14%) libc.so.6::__send (1,149,037,952 samples, 0.03%) [unknown] (1,149,037,952 samples, 0.03%) [unknown] (1,149,037,952 samples, 0.03%) [unknown] (1,149,037,952 samples, 0.03%) [unknown] (1,096,533,096 samples, 0.03%) [unknown] (1,096,533,096 samples, 0.03%) [unknown] (1,096,533,096 samples, 0.03%) [unknown] (1,094,640,456 samples, 0.03%) [unknown] (943,771,904 samples, 0.03%) [unknown] (626,496,659 samples, 0.02%) [unknown] (522,399,654 samples, 0.01%) [unknown] (469,549,544 samples, 0.01%) [unknown] (469,549,544 samples, 0.01%) [unknown] (366,321,373 samples, 0.01%) libc.so.6::_int_free (16,918,597,179 samples, 0.48%) libc.so.6::_int_free_merge_chunk (716,678,677 samples, 0.02%) libc.so.6::_int_malloc (1,269,524,481 samples, 0.04%) libc.so.6::cfree@GLIBC_2.2.5 (4,352,992,616 samples, 0.12%) libc.so.6::malloc (8,032,159,513 samples, 0.23%) libc.so.6::malloc_consolidate (39,479,511,598 samples, 1.11%) [unknown] (401,333,554 samples, 0.01%) [unknown] (401,333,554 samples, 0.01%) [unknown] (401,333,554 samples, 0.01%) [unknown] (401,333,554 samples, 0.01%) [unknown] (401,333,554 samples, 0.01%) [unknown] (401,333,554 samples, 0.01%) libc.so.6::new_do_write (469,906,341 samples, 0.01%) libc.so.6::read (459,442,054 samples, 0.01%) [unknown] (459,442,054 samples, 0.01%) [unknown] (360,200,514 samples, 0.01%) [unknown] (360,200,514 samples, 0.01%) [unknown] (360,200,514 samples, 0.01%) [unknown] (360,200,514 samples, 0.01%) libc.so.6::sysmalloc (469,717,952 samples, 0.01%) [unknown] (469,717,952 samples, 0.01%) [unknown] (415,893,983 samples, 0.01%) [unknown] (366,135,265 samples, 0.01%) [unknown] (366,135,265 samples, 0.01%) libc.so.6::unlink_chunk.isra.0 (2,862,604,776 samples, 0.08%) bitcoind::CBlockIndex::GetAncestor (412,360,660 samples, 0.01%) bitcoind::CCoinsViewCache::AccessCoin (421,783,849 samples, 0.01%) bitcoind::SipHashUint256Extra (6,150,872,313 samples, 0.17%) bitcoind::std::_Hashtable<COutPoint, std::pair<COutPoint const, CCoinsCacheEntry>, PoolAllocator<std::pair<COutPoint const, CCoinsCacheEntry>, 144ul, 8ul>, std::__detail::_Select1st, std::equal_to<COutPoint>, SaltedOutpointHasher, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<false, false, true> >::_M_erase (100,736,697,557 samples, 2.83%) bitc.. bitcoind::SipHashUint256Extra (1,991,693,392 samples, 0.06%) bitcoind::std::_Hashtable<COutPoint, std::pair<COutPoint const, CCoinsCacheEntry>, PoolAllocator<std::pair<COutPoint const, CCoinsCacheEntry>, 144ul, 8ul>, std::__detail::_Select1st, std::equal_to<COutPoint>, SaltedOutpointHasher, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<false, false, true> >::_M_find_before_node (90,084,545,787 samples, 2.53%) bit.. bitcoind::SipHashUint256Extra (71,251,854,599 samples, 2.00%) bi.. bitcoind::SipHashUint256Extra (26,794,756,611 samples, 0.75%) bitcoind::std::_Hashtable<COutPoint, std::pair<COutPoint const, CCoinsCacheEntry>, PoolAllocator<std::pair<COutPoint const, CCoinsCacheEntry>, 144ul, 8ul>, std::__detail::_Select1st, std::equal_to<COutPoint>, SaltedOutpointHasher, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<false, false, true> >::_M_insert_unique_node (46,369,997,648 samples, 1.30%) bitcoind::std::_Hashtable<COutPoint, std::pair<COutPoint const, CCoinsCacheEntry>, PoolAllocator<std::pair<COutPoint const, CCoinsCacheEntry>, 144ul, 8ul>, std::__detail::_Select1st, std::equal_to<COutPoint>, SaltedOutpointHasher, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<false, false, true> >::_M_rehash (18,471,505,609 samples, 0.52%) libc.so.6::__memset_avx512_unaligned_erms (632,105,655 samples, 0.02%) [unknown] (579,371,219 samples, 0.02%) [unknown] (474,387,191 samples, 0.01%) [unknown] (421,585,797 samples, 0.01%) [unknown] (421,585,797 samples, 0.01%) [unknown] (368,759,434 samples, 0.01%) [unknown] (368,759,434 samples, 0.01%) [unknown] (368,759,434 samples, 0.01%) bitcoind::std::_Hashtable<COutPoint, std::pair<COutPoint const, CCoinsCacheEntry>, PoolAllocator<std::pair<COutPoint const, CCoinsCacheEntry>, 144ul, 8ul>, std::__detail::_Select1st, std::equal_to<COutPoint>, SaltedOutpointHasher, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<false, false, true> >::erase (1,518,987,687 samples, 0.04%) bitcoind::SipHashUint256Extra (625,645,482 samples, 0.02%) bitcoind::SipHashUint256Extra (6,692,957,315 samples, 0.19%) [unknown] (1,036,177,296 samples, 0.03%) [unknown] (928,879,608 samples, 0.03%) [unknown] (877,183,919 samples, 0.02%) [unknown] (719,026,447 samples, 0.02%) [unknown] (666,701,067 samples, 0.02%) [unknown] (626,005,752 samples, 0.02%) [unknown] (364,282,815 samples, 0.01%) [unknown] (364,282,815 samples, 0.01%) [unknown] (364,282,815 samples, 0.01%) [unknown] (364,282,815 samples, 0.01%) bitcoind::std::_Hashtable<COutPoint, std::pair<COutPoint const, CCoinsCacheEntry>, PoolAllocator<std::pair<COutPoint const, CCoinsCacheEntry>, 144ul, 8ul>, std::__detail::_Select1st, std::equal_to<COutPoint>, SaltedOutpointHasher, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<false, false, true> >::find (133,163,328,034 samples, 3.75%) bitcoi.. bitcoind::std::_Hashtable<COutPoint, std::pair<COutPoint const, CCoinsCacheEntry>, PoolAllocator<std::pair<COutPoint const, CCoinsCacheEntry>, 144ul, 8ul>, std::__detail::_Select1st, std::equal_to<COutPoint>, SaltedOutpointHasher, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<false, false, true> >::_M_find_before_node (119,438,100,972 samples, 3.36%) bitco.. bitcoind::SipHashUint256Extra (986,497,657 samples, 0.03%) bitcoind::std::__detail::_Hash_node<std::pair<COutPoint const, CCoinsCacheEntry>, false>* std::__detail::_Hashtable_alloc<PoolAllocator<std::__detail::_Hash_node<std::pair<COutPoint const, CCoinsCacheEntry>, false>, 144ul, 8ul> >::_M_allocate_node<std::piecewise_construct_t const&, std::tuple<COutPoint const&>, std::tuple<> > (5,414,052,109 samples, 0.15%) libc.so.6::cfree@GLIBC_2.2.5 (4,527,272,747 samples, 0.13%) bitcoind::CCoinsViewCache::BatchWrite (408,297,908,928 samples, 11.48%) bitcoind::CCoinsViewCac.. bitcoind::std::_Hashtable<COutPoint, std::pair<COutPoint const, CCoinsCacheEntry>, PoolAllocator<std::pair<COutPoint const, CCoinsCacheEntry>, 144ul, 8ul>, std::__detail::_Select1st, std::equal_to<COutPoint>, SaltedOutpointHasher, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<false, false, true> >::clear (4,431,167,402 samples, 0.12%) bitcoind::std::_Hashtable<COutPoint, std::pair<COutPoint const, CCoinsCacheEntry>, PoolAllocator<std::pair<COutPoint const, CCoinsCacheEntry>, 144ul, 8ul>, std::__detail::_Select1st, std::equal_to<COutPoint>, SaltedOutpointHasher, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<false, false, true> >::find (676,133,697 samples, 0.02%) bitcoind::CCoinsViewCache::Flush (414,604,793,420 samples, 11.66%) bitcoind::CCoinsViewCach.. bitcoind::CTxMemPool::removeConflicts (1,307,189,422 samples, 0.04%) bitcoind::std::_Rb_tree<COutPoint const*, std::pair<COutPoint const* const, CTransaction const*>, std::_Select1st<std::pair<COutPoint const* const, CTransaction const*> >, DereferencingComparator<COutPoint const*>, std::allocator<std::pair<COutPoint const* const, CTransaction const*> > >::find (940,298,479 samples, 0.03%) bitcoind::SipHashUint256 (1,301,282,993 samples, 0.04%) bitcoind::std::_Rb_tree<uint256, std::pair<uint256 const, long>, std::_Select1st<std::pair<uint256 const, long> >, std::less<uint256>, std::allocator<std::pair<uint256 const, long> > >::_M_erase (1,201,625,005 samples, 0.03%) bitcoind::CTxMemPool::removeForBlock (17,028,655,239 samples, 0.48%) bitcoind::std::_Rb_tree<uint256, std::pair<uint256 const, long>, std::_Select1st<std::pair<uint256 const, long> >, std::less<uint256>, std::allocator<std::pair<uint256 const, long> > >::erase (12,855,923,134 samples, 0.36%) bitcoind::std::_Rb_tree<uint256, std::pair<uint256 const, long>, std::_Select1st<std::pair<uint256 const, long> >, std::less<uint256>, std::allocator<std::pair<uint256 const, long> > >::equal_range (2,508,971,022 samples, 0.07%) [unknown] (3,441,479,431 samples, 0.10%) [unknown] (3,089,709,936 samples, 0.09%) [unknown] (2,820,174,820 samples, 0.08%) [unknown] (2,720,356,939 samples, 0.08%) [unknown] (2,720,356,939 samples, 0.08%) [unknown] (2,557,087,196 samples, 0.07%) [unknown] (2,356,775,337 samples, 0.07%) [unknown] (1,672,816,080 samples, 0.05%) [unknown] (1,100,674,926 samples, 0.03%) [unknown] (787,217,059 samples, 0.02%) [unknown] (574,492,426 samples, 0.02%) bitcoind::SipHashUint256Extra (359,543,734 samples, 0.01%) bitcoind::std::_Hashtable<COutPoint, std::pair<COutPoint const, CCoinsCacheEntry>, PoolAllocator<std::pair<COutPoint const, CCoinsCacheEntry>, 144ul, 8ul>, std::__detail::_Select1st, std::equal_to<COutPoint>, SaltedOutpointHasher, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<false, false, true> >::_M_find_before_node (10,977,516,042 samples, 0.31%) bitcoind::SipHashUint256Extra (3,562,058,963 samples, 0.10%) bitcoind::SipHashUint256Extra (1,836,963,585 samples, 0.05%) bitcoind::SipHashUint256Extra (6,867,820,925 samples, 0.19%) bitcoind::std::_Hashtable<COutPoint, std::pair<COutPoint const, CCoinsCacheEntry>, PoolAllocator<std::pair<COutPoint const, CCoinsCacheEntry>, 144ul, 8ul>, std::__detail::_Select1st, std::equal_to<COutPoint>, SaltedOutpointHasher, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<false, false, true> >::_M_insert_unique_node (16,890,522,357 samples, 0.48%) bitcoind::std::_Hashtable<COutPoint, std::pair<COutPoint const, CCoinsCacheEntry>, PoolAllocator<std::pair<COutPoint const, CCoinsCacheEntry>, 144ul, 8ul>, std::__detail::_Select1st, std::equal_to<COutPoint>, SaltedOutpointHasher, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<false, false, true> >::_M_rehash (12,768,158,119 samples, 0.36%) bitcoind::std::__detail::_Hash_node<std::pair<COutPoint const, CCoinsCacheEntry>, false>* std::__detail::_Hashtable_alloc<PoolAllocator<std::__detail::_Hash_node<std::pair<COutPoint const, CCoinsCacheEntry>, false>, 144ul, 8ul> >::_M_allocate_node<std::piecewise_construct_t const&, std::tuple<COutPoint const&>, std::tuple<> > (6,083,575,685 samples, 0.17%) [unknown] (2,667,289,880 samples, 0.08%) [unknown] (2,453,773,220 samples, 0.07%) [unknown] (2,293,236,868 samples, 0.06%) [unknown] (2,189,852,142 samples, 0.06%) [unknown] (1,978,814,058 samples, 0.06%) [unknown] (1,713,021,112 samples, 0.05%) [unknown] (1,360,558,892 samples, 0.04%) [unknown] (1,099,770,850 samples, 0.03%) [unknown] (785,095,967 samples, 0.02%) [unknown] (468,560,942 samples, 0.01%) [unknown] (366,515,283 samples, 0.01%) bitcoind::CCoinsViewCache::AddCoin (67,517,205,631 samples, 1.90%) bi.. bitcoind::AddCoins (83,151,504,659 samples, 2.34%) bit.. bitcoind::std::__detail::_Hash_node<std::pair<COutPoint const, CCoinsCacheEntry>, false>* std::__detail::_Hashtable_alloc<PoolAllocator<std::__detail::_Hash_node<std::pair<COutPoint const, CCoinsCacheEntry>, false>, 144ul, 8ul> >::_M_allocate_node<std::piecewise_construct_t const&, std::tuple<COutPoint const&>, std::tuple<> > (368,308,911 samples, 0.01%) bitcoind::CBlockIndex::GetAncestor (780,828,411 samples, 0.02%) bitcoind::SipHashUint256Extra (6,967,127,022 samples, 0.20%) bitcoind::CCoinsViewCache::FetchCoin (11,631,656,359 samples, 0.33%) bitcoind::std::_Hashtable<COutPoint, std::pair<COutPoint const, CCoinsCacheEntry>, PoolAllocator<std::pair<COutPoint const, CCoinsCacheEntry>, 144ul, 8ul>, std::__detail::_Select1st, std::equal_to<COutPoint>, SaltedOutpointHasher, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<false, false, true> >::_M_find_before_node (2,762,447,333 samples, 0.08%) bitcoind::CCoinsViewCache::AccessCoin (13,718,933,582 samples, 0.39%) bitcoind::CCoinsViewCache::AddCoin (935,848,977 samples, 0.03%) bitcoind::CCoinsViewCache::HaveInputs (363,967,847 samples, 0.01%) bitcoind::CCoinsViewCache::SpendCoin (775,446,488 samples, 0.02%) bitcoind::CTransaction::GetValueOut (571,129,594 samples, 0.02%) bitcoind::SipHashUint256Extra (6,132,196,838 samples, 0.17%) bitcoind::CCoinsViewCache::FetchCoin (22,771,955,106 samples, 0.64%) bitcoind::std::_Hashtable<COutPoint, std::pair<COutPoint const, CCoinsCacheEntry>, PoolAllocator<std::pair<COutPoint const, CCoinsCacheEntry>, 144ul, 8ul>, std::__detail::_Select1st, std::equal_to<COutPoint>, SaltedOutpointHasher, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<false, false, true> >::_M_find_before_node (8,387,260,917 samples, 0.24%) bitcoind::SipHashUint256Extra (672,360,582 samples, 0.02%) bitcoind::CCoinsViewCache::AccessCoin (27,541,380,041 samples, 0.77%) bitcoind::std::_Hashtable<COutPoint, std::pair<COutPoint const, CCoinsCacheEntry>, PoolAllocator<std::pair<COutPoint const, CCoinsCacheEntry>, 144ul, 8ul>, std::__detail::_Select1st, std::equal_to<COutPoint>, SaltedOutpointHasher, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<false, false, true> >::_M_find_before_node (840,128,595 samples, 0.02%) bitcoind::CCoinsViewCache::FetchCoin (9,862,576,991 samples, 0.28%) bitcoind::CCoinsViewCache::FetchCoin (723,258,358 samples, 0.02%) bitcoind::CCoinsViewBacked::GetCoin (1,001,559,892 samples, 0.03%) bitcoind::leveldb::LookupKey::LookupKey (468,932,422 samples, 0.01%) bitcoind::leveldb::(anonymous namespace)::BytewiseComparatorImpl::Compare (477,889,771 samples, 0.01%) bitcoind::leveldb::(anonymous namespace)::BytewiseComparatorImpl::Compare (2,464,437,114 samples, 0.07%) bitcoind::leveldb::FindFile (12,889,348,897 samples, 0.36%) bitcoind::leveldb::InternalKeyComparator::Compare (8,952,657,039 samples, 0.25%) libc.so.6::__memcmp_evex_movbe (3,658,168,717 samples, 0.10%) bitcoind::leveldb::InternalKeyComparator::Compare (468,603,758 samples, 0.01%) bitcoind::leveldb::(anonymous namespace)::ShardedLRUCache::Lookup (2,481,353,143 samples, 0.07%) [unknown] (470,703,247 samples, 0.01%) [unknown] (419,110,322 samples, 0.01%) [unknown] (367,081,554 samples, 0.01%) [unknown] (367,081,554 samples, 0.01%) bitcoind::std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_mutate (723,558,367 samples, 0.02%) libc.so.6::__memmove_avx512_unaligned_erms (682,634,544 samples, 0.02%) bitcoind::leveldb::Block::Iter::ParseNextKey (6,607,693,428 samples, 0.19%) libc.so.6::malloc (468,621,157 samples, 0.01%) bitcoind::leveldb::(anonymous namespace)::BytewiseComparatorImpl::Compare (3,736,764,421 samples, 0.11%) bitcoind::leveldb::InternalKeyComparator::Compare (22,752,758,306 samples, 0.64%) libc.so.6::__memcmp_evex_movbe (16,502,022,326 samples, 0.46%) bitcoind::leveldb::Block::Iter::Seek (81,753,854,146 samples, 2.30%) bit.. libc.so.6::__memmove_avx512_unaligned_erms (624,754,079 samples, 0.02%) bitcoind::leveldb::Block::Iter::~Iter (1,202,042,453 samples, 0.03%) bitcoind::leveldb::Iterator::~Iterator (886,809,043 samples, 0.02%) bitcoind::leveldb::DeleteBlock (418,661,180 samples, 0.01%) bitcoind::leveldb::Block::NewIterator (1,830,741,267 samples, 0.05%) bitcoind::leveldb::BlockHandle::DecodeFrom (1,350,133,609 samples, 0.04%) bitcoind::leveldb::FilterBlockReader::KeyMayMatch (3,241,956,535 samples, 0.09%) bitcoind::leveldb::InternalFilterPolicy::KeyMayMatch (470,469,134 samples, 0.01%) bitcoind::leveldb::(anonymous namespace)::BloomFilterPolicy::KeyMayMatch (470,469,134 samples, 0.01%) bitcoind::leveldb::InternalKeyComparator::Compare (2,930,394,374 samples, 0.08%) bitcoind::leveldb::SaveValue (885,107,264 samples, 0.02%) bitcoind::leveldb::(anonymous namespace)::ShardedLRUCache::Lookup (1,152,034,360 samples, 0.03%) bitcoind::leveldb::Hash (363,890,191 samples, 0.01%) bitcoind::leveldb::Block::NewIterator (1,259,229,813 samples, 0.04%) bitcoind::leveldb::BlockHandle::DecodeFrom (1,156,612,863 samples, 0.03%) bitcoind::leveldb::GetVarint64 (416,693,035 samples, 0.01%) bitcoind::leveldb::Iterator::RegisterCleanup (363,166,691 samples, 0.01%) [unknown] (2,314,123,053 samples, 0.07%) [unknown] (2,156,687,800 samples, 0.06%) [unknown] (2,051,108,413 samples, 0.06%) [unknown] (1,945,393,833 samples, 0.05%) [unknown] (1,894,650,811 samples, 0.05%) [unknown] (1,894,650,811 samples, 0.05%) [unknown] (1,794,842,453 samples, 0.05%) [unknown] (1,315,291,384 samples, 0.04%) [unknown] (733,842,157 samples, 0.02%) [unknown] (421,059,647 samples, 0.01%) [unknown] (367,252,654 samples, 0.01%) bitcoind::crc32c::ExtendSse42 (56,521,776,403 samples, 1.59%) b.. bitcoind::leveldb::ReadBlock (62,722,682,079 samples, 1.76%) b.. libc.so.6::__GI___pthread_mutex_unlock_usercnt (978,769,336 samples, 0.03%) libc.so.6::cfree@GLIBC_2.2.5 (571,745,263 samples, 0.02%) bitcoind::leveldb::Table::BlockReader (93,027,689,265 samples, 2.62%) bit.. libc.so.6::__memmove_avx512_unaligned_erms (525,280,305 samples, 0.01%) bitcoind::leveldb::Table::InternalGet (191,009,481,478 samples, 5.37%) bitcoind::.. bitcoind::leveldb::(anonymous namespace)::ShardedLRUCache::Lookup (2,456,558,609 samples, 0.07%) bitcoind::leveldb::Hash (674,476,478 samples, 0.02%) libc.so.6::__GI___pthread_mutex_unlock_usercnt (949,827,762 samples, 0.03%) libc.so.6::__memcmp_evex_movbe (672,469,665 samples, 0.02%) libc.so.6::pthread_mutex_lock@@GLIBC_2.2.5 (770,697,666 samples, 0.02%) bitcoind::leveldb::TableCache::FindTable (5,889,647,371 samples, 0.17%) bitcoind::leveldb::TableCache::Get (199,229,141,358 samples, 5.60%) bitcoind::.. bitcoind::leveldb::Version::Get (200,226,855,069 samples, 5.63%) bitcoind::.. libc.so.6::__GI___pthread_mutex_unlock_usercnt (733,288,816 samples, 0.02%) bitcoind::leveldb::Version::ForEachOverlapping (215,208,197,899 samples, 6.05%) bitcoind::l.. libc.so.6::__memcmp_evex_movbe (359,285,284 samples, 0.01%) bitcoind::leveldb::Version::Get (216,049,507,027 samples, 6.08%) bitcoind::l.. bitcoind::leveldb::DBImpl::Get (217,672,929,621 samples, 6.12%) bitcoind::l.. libc.so.6::__GI___pthread_mutex_unlock_usercnt (1,861,877,233 samples, 0.05%) bitcoind::CDBWrapper::ReadImpl[abi:cxx11] (221,752,252,623 samples, 6.24%) bitcoind::CD.. libc.so.6::pthread_mutex_lock@@GLIBC_2.2.5 (1,748,433,964 samples, 0.05%) bitcoind::DecompressAmount (1,005,313,570 samples, 0.03%) bitcoind::void ScriptCompression::Unser<DataStream> (2,769,444,330 samples, 0.08%) bitcoind::void std::vector<std::byte, zero_after_free_allocator<std::byte> >::_M_range_insert<std::byte const*> (7,911,029,894 samples, 0.22%) libc.so.6::__memmove_avx512_unaligned_erms (416,410,569 samples, 0.01%) bitcoind::CCoinsViewDB::GetCoin (247,131,705,346 samples, 6.95%) bitcoind::CCo.. bitcoind::CCoinsViewBacked::GetCoin (251,714,610,750 samples, 7.08%) bitcoind::CCo.. bitcoind::CCoinsViewErrorCatcher::GetCoin (257,960,090,912 samples, 7.26%) bitcoind::CCoi.. bitcoind::CCoinsViewDB::GetCoin (5,789,812,101 samples, 0.16%) bitcoind::SipHashUint256Extra (686,778,601 samples, 0.02%) [unknown] (1,028,820,936 samples, 0.03%) [unknown] (974,950,139 samples, 0.03%) [unknown] (867,196,862 samples, 0.02%) [unknown] (710,030,298 samples, 0.02%) [unknown] (710,030,298 samples, 0.02%) [unknown] (600,430,034 samples, 0.02%) [unknown] (489,234,171 samples, 0.01%) [unknown] (434,975,120 samples, 0.01%) [unknown] (434,975,120 samples, 0.01%) bitcoind::std::_Hashtable<COutPoint, std::pair<COutPoint const, CCoinsCacheEntry>, PoolAllocator<std::pair<COutPoint const, CCoinsCacheEntry>, 144ul, 8ul>, std::__detail::_Select1st, std::equal_to<COutPoint>, SaltedOutpointHasher, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<false, false, true> >::_M_find_before_node (29,304,700,539 samples, 0.82%) bitcoind::std::_Hashtable<COutPoint, std::pair<COutPoint const, CCoinsCacheEntry>, PoolAllocator<std::pair<COutPoint const, CCoinsCacheEntry>, 144ul, 8ul>, std::__detail::_Select1st, std::equal_to<COutPoint>, SaltedOutpointHasher, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<false, false, true> >::_M_insert_unique_node (21,307,639,964 samples, 0.60%) bitcoind::std::_Hashtable<COutPoint, std::pair<COutPoint const, CCoinsCacheEntry>, PoolAllocator<std::pair<COutPoint const, CCoinsCacheEntry>, 144ul, 8ul>, std::__detail::_Select1st, std::equal_to<COutPoint>, SaltedOutpointHasher, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<false, false, true> >::_M_rehash (20,780,827,998 samples, 0.58%) libc.so.6::__memset_avx512_unaligned_erms (579,451,231 samples, 0.02%) [unknown] (579,451,231 samples, 0.02%) [unknown] (526,649,228 samples, 0.01%) [unknown] (526,649,228 samples, 0.01%) [unknown] (526,649,228 samples, 0.01%) [unknown] (473,772,435 samples, 0.01%) [unknown] (420,996,348 samples, 0.01%) [unknown] (368,735,591 samples, 0.01%) [unknown] (368,735,591 samples, 0.01%) bitcoind::std::__detail::_Hash_node<std::pair<COutPoint const, CCoinsCacheEntry>, false>* std::__detail::_Hashtable_alloc<PoolAllocator<std::__detail::_Hash_node<std::pair<COutPoint const, CCoinsCacheEntry>, false>, 144ul, 8ul> >::_M_allocate_node<std::piecewise_construct_t const&, std::tuple<COutPoint const&>, std::tuple<> > (4,934,629,385 samples, 0.14%) [unknown] (421,130,280 samples, 0.01%) [unknown] (368,737,467 samples, 0.01%) [unknown] (368,737,467 samples, 0.01%) bitcoind::CCoinsViewCache::FetchCoin (327,425,895,563 samples, 9.21%) bitcoind::CCoinsVi.. bitcoind::CCoinsViewErrorCatcher::GetCoin (601,145,923 samples, 0.02%) bitcoind::CCoinsViewCache::GetCoin (349,247,006,292 samples, 9.82%) bitcoind::CCoinsView.. bitcoind::SipHashUint256Extra (17,454,209,723 samples, 0.49%) bitcoind::std::_Hashtable<COutPoint, std::pair<COutPoint const, CCoinsCacheEntry>, PoolAllocator<std::pair<COutPoint const, CCoinsCacheEntry>, 144ul, 8ul>, std::__detail::_Select1st, std::equal_to<COutPoint>, SaltedOutpointHasher, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<false, false, true> >::_M_find_before_node (22,697,810,020 samples, 0.64%) bitcoind::SipHashUint256Extra (4,124,049,750 samples, 0.12%) bitcoind::SipHashUint256Extra (4,306,133,540 samples, 0.12%) bitcoind::SipHashUint256Extra (7,085,914,542 samples, 0.20%) bitcoind::std::_Hashtable<COutPoint, std::pair<COutPoint const, CCoinsCacheEntry>, PoolAllocator<std::pair<COutPoint const, CCoinsCacheEntry>, 144ul, 8ul>, std::__detail::_Select1st, std::equal_to<COutPoint>, SaltedOutpointHasher, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<false, false, true> >::_M_insert_unique_node (19,180,887,889 samples, 0.54%) bitcoind::std::_Hashtable<COutPoint, std::pair<COutPoint const, CCoinsCacheEntry>, PoolAllocator<std::pair<COutPoint const, CCoinsCacheEntry>, 144ul, 8ul>, std::__detail::_Select1st, std::equal_to<COutPoint>, SaltedOutpointHasher, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<false, false, true> >::_M_rehash (12,199,005,039 samples, 0.34%) libc.so.6::__memset_avx512_unaligned_erms (574,777,734 samples, 0.02%) bitcoind::std::__detail::_Hash_node<std::pair<COutPoint const, CCoinsCacheEntry>, false>* std::__detail::_Hashtable_alloc<PoolAllocator<std::__detail::_Hash_node<std::pair<COutPoint const, CCoinsCacheEntry>, false>, 144ul, 8ul> >::_M_allocate_node<std::piecewise_construct_t const&, std::tuple<COutPoint const&>, std::tuple<> > (7,865,255,678 samples, 0.22%) [unknown] (1,969,736,150 samples, 0.06%) [unknown] (1,916,111,977 samples, 0.05%) [unknown] (1,812,200,695 samples, 0.05%) [unknown] (1,812,200,695 samples, 0.05%) [unknown] (1,812,200,695 samples, 0.05%) [unknown] (1,496,076,465 samples, 0.04%) [unknown] (1,234,917,855 samples, 0.03%) [unknown] (921,179,131 samples, 0.03%) [unknown] (658,036,512 samples, 0.02%) [unknown] (507,636,670 samples, 0.01%) bitcoind::CCoinsViewCache::FetchCoin (439,862,693,437 samples, 12.37%) bitcoind::CCoinsViewCache.. bitcoind::CCoinsViewCache::GetCoin (567,408,453 samples, 0.02%) bitcoind::SipHashUint256Extra (11,079,411,759 samples, 0.31%) bitcoind::CCoinsViewCache::HaveInputs (468,021,622,384 samples, 13.16%) bitcoind::CCoinsViewCache::.. bitcoind::Consensus::CheckTxInputs (525,550,058,887 samples, 14.78%) bitcoind::Consensus::CheckTxInp.. bitcoind::CTransaction::GetValueOut (8,116,827,965 samples, 0.23%) bitcoind::EvaluateSequenceLocks (13,084,419,728 samples, 0.37%) bitcoind::CBlockIndex::GetMedianTimePast (12,762,378,539 samples, 0.36%) bitcoind::void std::__introsort_loop<long*, long, __gnu_cxx::__ops::_Iter_less_iter> (1,776,177,595 samples, 0.05%) bitcoind::SipHashUint256Extra (3,528,590,848 samples, 0.10%) bitcoind::CCoinsViewCache::FetchCoin (9,099,104,563 samples, 0.26%) bitcoind::std::_Hashtable<COutPoint, std::pair<COutPoint const, CCoinsCacheEntry>, PoolAllocator<std::pair<COutPoint const, CCoinsCacheEntry>, 144ul, 8ul>, std::__detail::_Select1st, std::equal_to<COutPoint>, SaltedOutpointHasher, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<false, false, true> >::_M_find_before_node (3,448,133,251 samples, 0.10%) bitcoind::SipHashUint256Extra (373,550,141 samples, 0.01%) bitcoind::CCoinsViewCache::AccessCoin (10,147,664,939 samples, 0.29%) bitcoind::std::_Hashtable<COutPoint, std::pair<COutPoint const, CCoinsCacheEntry>, PoolAllocator<std::pair<COutPoint const, CCoinsCacheEntry>, 144ul, 8ul>, std::__detail::_Select1st, std::equal_to<COutPoint>, SaltedOutpointHasher, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<false, false, true> >::_M_find_before_node (368,697,772 samples, 0.01%) bitcoind::CScript::GetSigOpCount (1,181,105,155 samples, 0.03%) bitcoind::CScript::IsPayToScriptHash (361,942,649 samples, 0.01%) bitcoind::CScript::IsPushOnly (1,550,137,517 samples, 0.04%) bitcoind::CScript::IsWitnessProgram (14,154,912,421 samples, 0.40%) bitcoind::GetScriptOp (1,727,592,712 samples, 0.05%) bitcoind::CScript::GetSigOpCount (1,617,517,251 samples, 0.05%) bitcoind::GetScriptOp (834,793,526 samples, 0.02%) bitcoind::WitnessSigOps (3,120,635,596 samples, 0.09%) bitcoind::CountWitnessSigOps (25,211,941,345 samples, 0.71%) bitcoind::CScript::GetSigOpCount (21,895,087,837 samples, 0.62%) bitcoind::GetScriptOp (11,871,223,047 samples, 0.33%) bitcoind::GetLegacySigOpCount (26,548,006,408 samples, 0.75%) bitcoind::GetScriptOp (1,822,747,918 samples, 0.05%) bitcoind::SipHashUint256Extra (1,613,835,917 samples, 0.05%) bitcoind::CCoinsViewCache::FetchCoin (6,631,397,326 samples, 0.19%) bitcoind::std::_Hashtable<COutPoint, std::pair<COutPoint const, CCoinsCacheEntry>, PoolAllocator<std::pair<COutPoint const, CCoinsCacheEntry>, 144ul, 8ul>, std::__detail::_Select1st, std::equal_to<COutPoint>, SaltedOutpointHasher, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<false, false, true> >::_M_find_before_node (2,817,298,340 samples, 0.08%) bitcoind::CCoinsViewCache::AccessCoin (7,316,792,317 samples, 0.21%) bitcoind::CCoinsViewCache::FetchCoin (363,943,746 samples, 0.01%) bitcoind::CScript::GetSigOpCount (1,160,904,417 samples, 0.03%) bitcoind::GetScriptOp (688,273,084 samples, 0.02%) bitcoind::GetScriptOp (2,964,048,193 samples, 0.08%) bitcoind::CScript::GetSigOpCount (5,643,658,755 samples, 0.16%) bitcoind::CScript::IsPayToScriptHash (581,631,871 samples, 0.02%) bitcoind::GetP2SHSigOpCount (15,633,133,461 samples, 0.44%) bitcoind::GetTransactionSigOpCost (84,183,784,739 samples, 2.37%) bit.. libstdc++.so.6.0.32::operator delete (405,410,027 samples, 0.01%) bitcoind::SequenceLocks (1,661,951,664 samples, 0.05%) bitcoind::CalculateSequenceLocks (1,453,270,225 samples, 0.04%) bitcoind::SipHashUint256Extra (937,441,713 samples, 0.03%) bitcoind::CCoinsViewCache::FetchCoin (2,049,216,208 samples, 0.06%) bitcoind::SipHashUint256Extra (1,345,870,966 samples, 0.04%) bitcoind::std::_Hashtable<COutPoint, std::pair<COutPoint const, CCoinsCacheEntry>, PoolAllocator<std::pair<COutPoint const, CCoinsCacheEntry>, 144ul, 8ul>, std::__detail::_Select1st, std::equal_to<COutPoint>, SaltedOutpointHasher, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<false, false, true> >::_M_erase (363,086,362 samples, 0.01%) bitcoind::CCoinsViewCache::SpendCoin (20,676,663,595 samples, 0.58%) bitcoind::std::_Hashtable<COutPoint, std::pair<COutPoint const, CCoinsCacheEntry>, PoolAllocator<std::pair<COutPoint const, CCoinsCacheEntry>, 144ul, 8ul>, std::__detail::_Select1st, std::equal_to<COutPoint>, SaltedOutpointHasher, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<false, false, true> >::erase (2,777,265,349 samples, 0.08%) bitcoind::SipHashUint256Extra (1,428,091,877 samples, 0.04%) bitcoind::UpdateCoins (24,385,621,354 samples, 0.69%) bitcoind::std::_Hashtable<COutPoint, std::pair<COutPoint const, CCoinsCacheEntry>, PoolAllocator<std::pair<COutPoint const, CCoinsCacheEntry>, 144ul, 8ul>, std::__detail::_Select1st, std::equal_to<COutPoint>, SaltedOutpointHasher, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<false, false, true> >::erase (473,710,256 samples, 0.01%) bitcoind::AutoFile::write (1,453,030,200 samples, 0.04%) bitcoind::CSHA256::Write (1,519,729,645 samples, 0.04%) bitcoind::CompressAmount (618,711,609 samples, 0.02%) bitcoind::CompressScript (985,913,050 samples, 0.03%) [[ext4]] (404,866,263 samples, 0.01%) bitcoind::node::BlockManager::FindUndoPos (561,722,604 samples, 0.02%) bitcoind::FlatFileSeq::Allocate (509,374,850 samples, 0.01%) libc.so.6::posix_fallocate (509,374,850 samples, 0.01%) [unknown] (509,374,850 samples, 0.01%) [unknown] (509,374,850 samples, 0.01%) [unknown] (509,374,850 samples, 0.01%) [unknown] (457,299,763 samples, 0.01%) bitcoind::AutoFile::write (10,042,610,399 samples, 0.28%) bitcoind::CSHA256::Write (19,844,383,315 samples, 0.56%) bitcoind::sha256_x86_shani::Transform (3,151,148,807 samples, 0.09%) bitcoind::CompressAmount (1,773,668,392 samples, 0.05%) bitcoind::CompressScript (4,638,408,540 samples, 0.13%) bitcoind::prevector<33u, unsigned char, unsigned int, int>::resize (3,040,914,869 samples, 0.09%) bitcoind::CompressAmount (831,251,028 samples, 0.02%) bitcoind::prevector<33u, unsigned char, unsigned int, int>::resize (1,914,945,145 samples, 0.05%) bitcoind::void VectorFormatter<DefaultFormatter>::Ser<SizeComputer, std::vector<CTxUndo, std::allocator<CTxUndo> > > (7,020,871,233 samples, 0.20%) bitcoind::CompressScript (2,957,454,406 samples, 0.08%) bitcoind::AutoFile::write (4,887,544,250 samples, 0.14%) bitcoind::void WriteVarInt<AutoFile, (VarIntMode)0, unsigned int> (5,868,765,238 samples, 0.17%) bitcoind::CSHA256::Write (8,012,816,481 samples, 0.23%) bitcoind::sha256_x86_shani::Transform (938,301,513 samples, 0.03%) bitcoind::void WriteVarInt<HashWriter, (VarIntMode)0, unsigned int> (12,386,753,309 samples, 0.35%) libc.so.6::__memmove_avx512_unaligned_erms (941,007,723 samples, 0.03%) libc.so.6::_IO_fwrite (1,409,554,078 samples, 0.04%) bitcoind::node::BlockManager::UndoWriteToDisk (74,178,487,109 samples, 2.09%) bi.. libc.so.6::__memmove_avx512_unaligned_erms (3,806,477,393 samples, 0.11%) bitcoind::CompressAmount (730,340,863 samples, 0.02%) bitcoind::void VectorFormatter<DefaultFormatter>::Ser<SizeComputer, std::vector<CTxUndo, std::allocator<CTxUndo> > > (9,108,229,147 samples, 0.26%) bitcoind::CompressScript (3,027,453,269 samples, 0.09%) bitcoind::prevector<33u, unsigned char, unsigned int, int>::resize (2,034,465,890 samples, 0.06%) bitcoind::void WriteVarInt<AutoFile, (VarIntMode)0, unsigned int> (367,022,852 samples, 0.01%) bitcoind::void WriteVarInt<HashWriter, (VarIntMode)0, unsigned int> (521,478,522 samples, 0.01%) bitcoind::node::BlockManager::WriteUndoDataForBlock (89,569,504,650 samples, 2.52%) bit.. bitcoind::std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_dispose (583,464,405 samples, 0.02%) libc.so.6::malloc (1,716,514,762 samples, 0.05%) bitcoind::Chainstate::ConnectBlock (855,466,273,851 samples, 24.06%) bitcoind::Chainstate::ConnectBlock bitcoind::std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_dispose (418,091,278 samples, 0.01%) libc.so.6::cfree@GLIBC_2.2.5 (1,144,767,280 samples, 0.03%) bitcoind::Chainstate::ConnectTip (1,291,793,481,748 samples, 36.33%) bitcoind::Chainstate::ConnectTip libstdc++.so.6.0.32::operator delete (627,918,999 samples, 0.02%) bitcoind::Chainstate::ActivateBestChainStep (1,291,995,942,063 samples, 36.34%) bitcoind::Chainstate::ActivateBestChainStep bitcoind::Chainstate::ActivateBestChain (1,292,515,820,515 samples, 36.35%) bitcoind::Chainstate::ActivateBestChain bitcoind::IsFinalTx (467,637,167 samples, 0.01%) bitcoind::void SerializeTransaction<ParamsStream<SizeComputer&, TransactionSerParams>, CTransaction> (25,890,452,766 samples, 0.73%) bitcoind::void WriteCompactSize<ParamsStream<SizeComputer&, TransactionSerParams> > (6,846,762,723 samples, 0.19%) bitcoind::ContextualCheckBlock (27,706,291,261 samples, 0.78%) bitcoind::void WriteCompactSize<ParamsStream<SizeComputer&, TransactionSerParams> > (1,089,597,648 samples, 0.03%) [[ext4]] (5,576,796,020 samples, 0.16%) [unknown] (3,815,201,507 samples, 0.11%) [unknown] (2,345,433,446 samples, 0.07%) [unknown] (520,783,293 samples, 0.01%) [[ext4]] (7,954,320,588 samples, 0.22%) [unknown] (1,662,013,865 samples, 0.05%) [unknown] (1,269,240,468 samples, 0.04%) [unknown] (705,348,263 samples, 0.02%) [unknown] (455,918,938 samples, 0.01%) [[nvme]] (807,919,787 samples, 0.02%) [[nvme]] (807,919,787 samples, 0.02%) [unknown] (807,919,787 samples, 0.02%) [unknown] (807,919,787 samples, 0.02%) [unknown] (807,919,787 samples, 0.02%) [unknown] (547,049,759 samples, 0.02%) [unknown] (496,243,932 samples, 0.01%) [unknown] (448,114,949 samples, 0.01%) [[ext4]] (12,810,206,632 samples, 0.36%) [unknown] (3,316,731,307 samples, 0.09%) [unknown] (2,036,481,321 samples, 0.06%) [unknown] (1,478,602,939 samples, 0.04%) [unknown] (1,322,893,322 samples, 0.04%) [unknown] (1,227,580,922 samples, 0.03%) [[ext4]] (13,121,603,080 samples, 0.37%) [[ext4]] (13,121,603,080 samples, 0.37%) bitcoind::FlatFileSeq::Flush (13,525,287,477 samples, 0.38%) libc.so.6::fdatasync (13,525,287,477 samples, 0.38%) [unknown] (13,525,287,477 samples, 0.38%) [unknown] (13,525,287,477 samples, 0.38%) [unknown] (13,525,287,477 samples, 0.38%) [[ext4]] (13,525,287,477 samples, 0.38%) [unknown] (13,525,287,477 samples, 0.38%) [unknown] (13,525,287,477 samples, 0.38%) [unknown] (13,525,287,477 samples, 0.38%) [unknown] (13,525,287,477 samples, 0.38%) [unknown] (403,684,397 samples, 0.01%) [unknown] (403,684,397 samples, 0.01%) [unknown] (403,684,397 samples, 0.01%) [[ext4]] (619,895,319 samples, 0.02%) [unknown] (483,644,425 samples, 0.01%) [[ext4]] (981,510,072 samples, 0.03%) [[ext4]] (1,916,459,846 samples, 0.05%) [unknown] (398,097,615 samples, 0.01%) [[ext4]] (1,967,105,500 samples, 0.06%) [[ext4]] (1,967,105,500 samples, 0.06%) bitcoind::node::BlockManager::FindNextBlockPos (16,065,033,072 samples, 0.45%) bitcoind::node::BlockManager::FlushBlockFile (15,700,018,553 samples, 0.44%) bitcoind::node::BlockManager::FlushUndoFile (2,174,731,076 samples, 0.06%) bitcoind::FlatFileSeq::Flush (2,174,731,076 samples, 0.06%) libc.so.6::fdatasync (2,174,731,076 samples, 0.06%) [unknown] (2,174,731,076 samples, 0.06%) [unknown] (2,174,731,076 samples, 0.06%) [unknown] (2,174,731,076 samples, 0.06%) [[ext4]] (2,174,731,076 samples, 0.06%) [unknown] (2,174,731,076 samples, 0.06%) [unknown] (2,174,731,076 samples, 0.06%) [unknown] (2,174,731,076 samples, 0.06%) [unknown] (2,119,891,081 samples, 0.06%) bitcoind::AutoFile::write (5,548,941,818 samples, 0.16%) libc.so.6::__GI___fstatat64 (365,833,677 samples, 0.01%) bitcoind::node::BlockManager::OpenBlockFile (470,569,767 samples, 0.01%) bitcoind::AutoFile::write (61,167,375,809 samples, 1.72%) b.. [unknown] (598,511,547 samples, 0.02%) [unknown] (457,806,853 samples, 0.01%) [unknown] (457,806,853 samples, 0.01%) [unknown] (457,806,853 samples, 0.01%) [unknown] (409,659,414 samples, 0.01%) [unknown] (357,939,661 samples, 0.01%) bitcoind::AutoFile::write (4,647,493,060 samples, 0.13%) bitcoind::void WriteCompactSize<ParamsStream<AutoFile&, TransactionSerParams> > (6,592,272,733 samples, 0.19%) libc.so.6::_IO_fwrite (910,505,012 samples, 0.03%) bitcoind::void SerializeMany<ParamsStream<AutoFile&, TransactionSerParams>, CBlockHeader, std::vector<std::shared_ptr<CTransaction const>, std::allocator<std::shared_ptr<CTransaction const> > > > (82,131,751,453 samples, 2.31%) bit.. libc.so.6::_IO_fwrite (8,527,040,897 samples, 0.24%) bitcoind::void SerializeMany<ParamsStream<SizeComputer&, TransactionSerParams>, CBlockHeader, std::vector<std::shared_ptr<CTransaction const>, std::allocator<std::shared_ptr<CTransaction const> > > > (15,937,770,258 samples, 0.45%) bitcoind::void WriteCompactSize<ParamsStream<SizeComputer&, TransactionSerParams> > (3,905,423,573 samples, 0.11%) bitcoind::void WriteCompactSize<ParamsStream<AutoFile&, TransactionSerParams> > (571,858,007 samples, 0.02%) bitcoind::node::BlockManager::WriteBlockToDisk (106,357,642,754 samples, 2.99%) bitc.. bitcoind::void WriteCompactSize<ParamsStream<SizeComputer&, TransactionSerParams> > (1,696,751,451 samples, 0.05%) bitcoind::void SerializeMany<ParamsStream<SizeComputer&, TransactionSerParams>, CBlockHeader, std::vector<std::shared_ptr<CTransaction const>, std::allocator<std::shared_ptr<CTransaction const> > > > (17,168,001,989 samples, 0.48%) bitcoind::void WriteCompactSize<ParamsStream<SizeComputer&, TransactionSerParams> > (4,825,716,114 samples, 0.14%) bitcoind::node::BlockManager::SaveBlockToDisk (141,154,624,112 samples, 3.97%) bitcoi.. bitcoind::void WriteCompactSize<ParamsStream<SizeComputer&, TransactionSerParams> > (1,511,914,109 samples, 0.04%) bitcoind::ChainstateManager::AcceptBlock (169,805,644,100 samples, 4.78%) bitcoind.. bitcoind::void SerializeTransaction<ParamsStream<SizeComputer&, TransactionSerParams>, CTransaction> (419,732,705 samples, 0.01%) bitcoind::CScript::GetSigOpCount (1,244,733,942 samples, 0.04%) bitcoind::memcmp@plt (416,583,431 samples, 0.01%) bitcoind::std::_Rb_tree<COutPoint, COutPoint, std::_Identity<COutPoint>, std::less<COutPoint>, std::allocator<COutPoint> >::_M_erase (1,490,186,398 samples, 0.04%) bitcoind::std::pair<std::_Rb_tree_iterator<COutPoint>, bool> std::_Rb_tree<COutPoint, COutPoint, std::_Identity<COutPoint>, std::less<COutPoint>, std::allocator<COutPoint> >::_M_insert_unique<COutPoint const&> (4,247,810,353 samples, 0.12%) bitcoind::void WriteCompactSize<ParamsStream<SizeComputer&, TransactionSerParams> > (781,801,877 samples, 0.02%) libc.so.6::__memcmp_evex_movbe (6,070,441,149 samples, 0.17%) libc.so.6::cfree@GLIBC_2.2.5 (421,482,290 samples, 0.01%) libstdc++.so.6.0.32::operator delete (614,232,991 samples, 0.02%) bitcoind::CheckTransaction (25,650,523,240 samples, 0.72%) libstdc++.so.6.0.32::std::_Rb_tree_insert_and_rebalance (2,281,327,330 samples, 0.06%) bitcoind::CScript::GetSigOpCount (19,161,186,078 samples, 0.54%) bitcoind::GetScriptOp (8,992,060,021 samples, 0.25%) bitcoind::GetLegacySigOpCount (22,614,517,690 samples, 0.64%) bitcoind::GetScriptOp (1,176,069,512 samples, 0.03%) bitcoind::std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_dispose (1,349,955,285 samples, 0.04%) bitcoind::void SerializeTransaction<ParamsStream<SizeComputer&, TransactionSerParams>, CTransaction> (6,676,130,736 samples, 0.19%) bitcoind::void WriteCompactSize<ParamsStream<SizeComputer&, TransactionSerParams> > (1,329,967,416 samples, 0.04%) bitcoind::CheckBlock (60,550,319,748 samples, 1.70%) b.. bitcoind::void WriteCompactSize<ParamsStream<SizeComputer&, TransactionSerParams> > (572,803,846 samples, 0.02%) bitcoind::ChainstateManager::ProcessNewBlock (1,523,688,403,640 samples, 42.85%) bitcoind::ChainstateManager::ProcessNewBlock bitcoind::sha256d64_x86_shani::Transform_2way (15,194,464,935 samples, 0.43%) bitcoind::BlockMerkleRoot (16,172,687,252 samples, 0.45%) bitcoind::ComputeMerkleRoot (15,499,928,925 samples, 0.44%) bitcoind::SHA256D64 (15,246,405,066 samples, 0.43%) bitcoind::CheckMerkleRoot (16,532,547,442 samples, 0.46%) libc.so.6::__memset_avx512_unaligned_erms (359,860,190 samples, 0.01%) bitcoind::sha256d64_x86_shani::Transform_2way (12,972,294,835 samples, 0.36%) bitcoind::SHA256D64 (13,025,009,373 samples, 0.37%) bitcoind::IsBlockMutated (30,129,022,002 samples, 0.85%) bitcoind::CheckWitnessMalleation (13,596,474,560 samples, 0.38%) bitcoind::BlockWitnessMerkleRoot (13,596,474,560 samples, 0.38%) bitcoind::ComputeMerkleRoot (13,077,728,889 samples, 0.37%) bitcoind::void (anonymous namespace)::PeerManagerImpl::MakeAndPushMessage<std::vector<CInv, std::allocator<CInv> >&> (406,479,193 samples, 0.01%) bitcoind::CConnman::PushMessage (406,479,193 samples, 0.01%) bitcoind::std::vector<unsigned char, std::allocator<unsigned char> >::_M_default_append (367,056,757 samples, 0.01%) bitcoind::unsigned long ReadCompactSize<ParamsStream<DataStream&, TransactionSerParams> > (622,762,372 samples, 0.02%) bitcoind::CTransaction::ComputeHasWitness (1,387,667,716 samples, 0.04%) bitcoind::CSHA256::Write (17,955,645,390 samples, 0.51%) bitcoind::sha256_x86_shani::Transform (11,932,913,194 samples, 0.34%) bitcoind::memcpy@plt (418,918,061 samples, 0.01%) bitcoind::sha256_x86_shani::Transform (3,306,980,273 samples, 0.09%) bitcoind::CSHA256::Finalize (22,917,960,073 samples, 0.64%) libc.so.6::__memmove_avx512_unaligned_erms (668,127,949 samples, 0.02%) bitcoind::CSHA256::Write (3,163,584,691 samples, 0.09%) bitcoind::CSHA256::Write (33,313,763,000 samples, 0.94%) bitcoind::sha256_x86_shani::Transform (14,194,928,537 samples, 0.40%) bitcoind::sha256_x86_shani::Transform (767,994,599 samples, 0.02%) bitcoind::CSHA256::Write (5,341,265,376 samples, 0.15%) bitcoind::void WriteCompactSize<ParamsStream<HashWriter&, TransactionSerParams> > (7,984,745,468 samples, 0.22%) bitcoind::void SerializeTransaction<ParamsStream<HashWriter&, TransactionSerParams>, CTransaction> (50,933,406,220 samples, 1.43%) b.. libc.so.6::__memmove_avx512_unaligned_erms (5,183,727,187 samples, 0.15%) bitcoind::void WriteCompactSize<ParamsStream<HashWriter&, TransactionSerParams> > (1,613,593,834 samples, 0.05%) bitcoind::CTransaction::ComputeHash (80,845,793,271 samples, 2.27%) bit.. bitcoind::CSHA256::Write (23,348,148,278 samples, 0.66%) bitcoind::sha256_x86_shani::Transform (11,595,812,714 samples, 0.33%) bitcoind::CSHA256::Finalize (24,335,325,870 samples, 0.68%) bitcoind::CSHA256::Write (2,288,432,816 samples, 0.06%) bitcoind::CSHA256::Write (64,681,112,465 samples, 1.82%) bi.. bitcoind::sha256_x86_shani::Transform (33,677,349,718 samples, 0.95%) bitcoind::sha256_x86_shani::Transform (622,627,277 samples, 0.02%) bitcoind::CSHA256::Write (11,395,509,513 samples, 0.32%) bitcoind::sha256_x86_shani::Transform (523,186,685 samples, 0.01%) bitcoind::void WriteCompactSize<ParamsStream<HashWriter&, TransactionSerParams> > (17,046,149,334 samples, 0.48%) libc.so.6::__memmove_avx512_unaligned_erms (2,169,704,353 samples, 0.06%) bitcoind::void SerializeTransaction<ParamsStream<HashWriter&, TransactionSerParams>, CTransaction> (92,366,151,212 samples, 2.60%) bit.. libc.so.6::__memmove_avx512_unaligned_erms (5,813,350,330 samples, 0.16%) bitcoind::void WriteCompactSize<ParamsStream<HashWriter&, TransactionSerParams> > (2,181,533,875 samples, 0.06%) bitcoind::CTransaction::ComputeWitnessHash (122,098,239,092 samples, 3.43%) bitco.. bitcoind::CTransaction::CTransaction (213,407,475,563 samples, 6.00%) bitcoind::C.. bitcoind::CTransaction::ComputeHasWitness (420,121,661 samples, 0.01%) bitcoind::CTransaction::ComputeHash (409,129,353 samples, 0.01%) bitcoind::DataStream::read (3,530,026,319 samples, 0.10%) bitcoind::operator new (628,502,415 samples, 0.02%) bitcoind::std::vector<unsigned char, std::allocator<unsigned char> >::_M_default_append (6,309,702,925 samples, 0.18%) bitcoind::unsigned long ReadCompactSize<ParamsStream<DataStream&, TransactionSerParams> > (4,193,618,734 samples, 0.12%) bitcoind::void Unserialize<ParamsStream<DataStream&, TransactionSerParams>, 28u, unsigned char> (727,834,750 samples, 0.02%) bitcoind::unsigned long ReadCompactSize<ParamsStream<DataStream&, TransactionSerParams> > (3,066,020,716 samples, 0.09%) bitcoind::void Unserialize<ParamsStream<DataStream&, TransactionSerParams>, 28u, unsigned char> (3,105,560,893 samples, 0.09%) bitcoind::unsigned long ReadCompactSize<ParamsStream<DataStream&, TransactionSerParams> > (1,455,846,726 samples, 0.04%) libc.so.6::__memmove_avx512_unaligned_erms (465,834,593 samples, 0.01%) bitcoind::void VectorFormatter<DefaultFormatter>::Unser<ParamsStream<DataStream&, TransactionSerParams>, std::vector<CTxIn, std::allocator<CTxIn> > > (15,677,650,112 samples, 0.44%) bitcoind::unsigned long ReadCompactSize<ParamsStream<DataStream&, TransactionSerParams> > (1,078,029,303 samples, 0.03%) bitcoind::void Unserialize<ParamsStream<DataStream&, TransactionSerParams>, 28u, unsigned char> (6,171,925,860 samples, 0.17%) bitcoind::unsigned long ReadCompactSize<ParamsStream<DataStream&, TransactionSerParams> > (982,486,879 samples, 0.03%) libc.so.6::__memmove_avx512_unaligned_erms (1,458,516,290 samples, 0.04%) bitcoind::void VectorFormatter<DefaultFormatter>::Unser<ParamsStream<DataStream&, TransactionSerParams>, std::vector<CTxOut, std::allocator<CTxOut> > > (13,963,877,725 samples, 0.39%) libc.so.6::__memmove_avx512_unaligned_erms (1,048,169,614 samples, 0.03%) libc.so.6::__memset_avx512_unaligned (1,046,482,105 samples, 0.03%) libc.so.6::__memset_avx512_unaligned_erms (1,963,080,141 samples, 0.06%) libc.so.6::malloc (3,025,102,825 samples, 0.09%) libstdc++.so.6.0.32::malloc@plt (1,462,651,744 samples, 0.04%) bitcoind::void Unserialize<ParamsStream<DataStream&, TransactionSerParams>, CTransaction> (283,898,001,379 samples, 7.98%) bitcoind::void U.. libstdc++.so.6.0.32::operator new (2,257,486,798 samples, 0.06%) bitcoind::void VectorFormatter<DefaultFormatter>::Unser<ParamsStream<DataStream&, TransactionSerParams>, std::vector<CTxIn, std::allocator<CTxIn> > > (1,090,260,916 samples, 0.03%) libc.so.6::__memmove_avx512_unaligned_erms (2,753,503,546 samples, 0.08%) libc.so.6::malloc (1,149,716,024 samples, 0.03%) bitcoind::void ParamsWrapper<TransactionSerParams, CBlock>::Unserialize<DataStream> (291,189,121,636 samples, 8.19%) bitcoind::void P.. bitcoind::void VectorFormatter<DefaultFormatter>::Unser<ParamsStream<DataStream&, TransactionSerParams>, std::vector<std::shared_ptr<CTransaction const>, std::allocator<std::shared_ptr<CTransaction const> > > > (291,134,772,004 samples, 8.19%) bitcoind::void V.. libstdc++.so.6.0.32::operator new (518,854,210 samples, 0.01%) libc.so.6::__memset_avx512_unaligned_erms (3,588,859,593 samples, 0.10%) bitcoind::CConnman::ThreadMessageHandler (1,852,055,734,561 samples, 52.09%) bitcoind::CConnman::ThreadMessageHandler libstdc++.so.6.0.32::execute_native_thread_routine (1,852,107,180,016 samples, 52.09%) libstdc++.so.6.0.32::execute_native_thread_routine bitcoind::std::thread::_State_impl<std::thread::_Invoker<std::tuple<void (*)(std::basic_string_view<char, std::char_traits<char> >, std::function<void ()>), char const*, CConnman::Start(CScheduler&, CConnman::Options const&)::{lambda()#5}> > >::_M_run (1,852,107,180,016 samples, 52.09%) bitcoind::std::thread::_State_impl<std::thread::_Invoker<std::tuple<void (*)(std::basic_string_view<char, std::char.. bitcoind::util::TraceThread (1,852,107,180,016 samples, 52.09%) bitcoind::util::TraceThread libstdc++.so.6.0.32::std::__cxx11::basic_stringbuf<char, std::char_traits<char>, std::allocator<char> >::overflow (397,900,679 samples, 0.01%) b-msghand (2,401,934,372,954 samples, 67.55%) b-msghand [[igc]] (638,737,826 samples, 0.02%) [unknown] (492,740,386 samples, 0.01%) [unknown] (492,740,386 samples, 0.01%) [unknown] (492,740,386 samples, 0.01%) libc.so.6::__libc_recv (23,769,090,268 samples, 0.67%) [unknown] (23,681,676,959 samples, 0.67%) [unknown] (23,585,908,630 samples, 0.66%) [unknown] (23,544,049,599 samples, 0.66%) [unknown] (23,499,819,825 samples, 0.66%) [unknown] (23,453,162,931 samples, 0.66%) [unknown] (23,205,326,716 samples, 0.65%) [unknown] (23,046,242,743 samples, 0.65%) [unknown] (23,000,657,790 samples, 0.65%) [unknown] (22,592,454,604 samples, 0.64%) [unknown] (21,715,983,496 samples, 0.61%) [unknown] (20,537,782,242 samples, 0.58%) [unknown] (19,311,079,312 samples, 0.54%) [unknown] (6,108,735,942 samples, 0.17%) [unknown] (1,360,583,546 samples, 0.04%) bitcoind::std::vector<std::byte, zero_after_free_allocator<std::byte> >::_M_fill_insert (16,619,401,507 samples, 0.47%) bitcoind::V2Transport::GetReceivedMessage (16,718,730,797 samples, 0.47%) [[igc]] (507,437,414 samples, 0.01%) [unknown] (412,781,498 samples, 0.01%) bitcoind::ChaCha20::Crypt (134,944,431,601 samples, 3.80%) bitcoi.. bitcoind::ChaCha20Aligned::Crypt (134,944,431,601 samples, 3.80%) bitcoi.. [unknown] (955,536,462 samples, 0.03%) [unknown] (955,536,462 samples, 0.03%) [unknown] (906,440,192 samples, 0.03%) [unknown] (861,463,927 samples, 0.02%) [unknown] (760,654,093 samples, 0.02%) [unknown] (658,510,836 samples, 0.02%) bitcoind::BIP324Cipher::Decrypt (196,638,059,936 samples, 5.53%) bitcoind::.. bitcoind::FSChaCha20Poly1305::Decrypt (196,638,059,936 samples, 5.53%) bitcoind::.. bitcoind::AEADChaCha20Poly1305::Decrypt (196,638,059,936 samples, 5.53%) bitcoind::.. bitcoind::poly1305_donna::poly1305_update (61,693,628,335 samples, 1.74%) b.. bitcoind::poly1305_donna::poly1305_blocks (61,693,628,335 samples, 1.74%) b.. [unknown] (655,063,915 samples, 0.02%) [unknown] (607,270,235 samples, 0.02%) [unknown] (525,964,847 samples, 0.01%) [unknown] (525,964,847 samples, 0.01%) [unknown] (470,111,416 samples, 0.01%) [unknown] (470,107,658 samples, 0.01%) bitcoind::V2Transport::ProcessReceivedPacketBytes (198,460,164,481 samples, 5.58%) bitcoind::.. libc.so.6::__memset_avx512_unaligned_erms (1,781,860,401 samples, 0.05%) bitcoind::V2Transport::ReceivedBytes (203,432,631,557 samples, 5.72%) bitcoind::.. libc.so.6::__memmove_avx512_unaligned_erms (4,655,332,308 samples, 0.13%) libc.so.6::__memmove_avx512_unaligned_erms (10,715,799,436 samples, 0.30%) bitcoind::CNode::ReceiveMsgBytes (231,225,287,054 samples, 6.50%) bitcoind::CN.. bitcoind::CConnman::SocketHandlerConnected (231,463,366,433 samples, 6.51%) bitcoind::CC.. libc.so.6::__poll (3,830,838,327 samples, 0.11%) [unknown] (3,830,838,327 samples, 0.11%) [unknown] (3,782,920,191 samples, 0.11%) [unknown] (3,725,807,764 samples, 0.10%) [unknown] (3,522,157,004 samples, 0.10%) [unknown] (3,150,768,515 samples, 0.09%) [unknown] (2,627,277,437 samples, 0.07%) [unknown] (2,338,467,135 samples, 0.07%) [unknown] (2,037,878,870 samples, 0.06%) [unknown] (1,480,962,324 samples, 0.04%) [unknown] (688,242,613 samples, 0.02%) bitcoind::CConnman::SocketHandler (236,436,484,949 samples, 6.65%) bitcoind::CCo.. b-net (260,905,688,952 samples, 7.34%) b-net libstdc++.so.6.0.32::execute_native_thread_routine (236,875,778,634 samples, 6.66%) libstdc++.so... bitcoind::std::thread::_State_impl<std::thread::_Invoker<std::tuple<void (*)(std::basic_string_view<char, std::char_traits<char> >, std::function<void ()>), char const*, CConnman::Start(CScheduler&, CConnman::Options const&)::{lambda()#1}> > >::_M_run (236,875,778,634 samples, 6.66%) bitcoind::std.. bitcoind::util::TraceThread (236,875,778,634 samples, 6.66%) bitcoind::uti.. bitcoind::CConnman::ThreadSocketHandler (236,875,778,634 samples, 6.66%) bitcoind::CCo.. libc.so.6::_int_free_create_chunk (982,572,444 samples, 0.03%) libc.so.6::_int_free_merge_chunk (797,147,451 samples, 0.02%) [unknown] (2,170,942,655 samples, 0.06%) libc.so.6::__futex_abstimed_wait_common (459,293,920 samples, 0.01%) [unknown] (459,293,920 samples, 0.01%) [unknown] (405,487,988 samples, 0.01%) [unknown] (405,482,438 samples, 0.01%) [unknown] (405,482,438 samples, 0.01%) [unknown] (356,784,451 samples, 0.01%) libc.so.6::__lll_lock_wait_private (57,276,007,979 samples, 1.61%) l.. [unknown] (54,932,210,267 samples, 1.54%) [.. [unknown] (52,306,124,993 samples, 1.47%) [.. [unknown] (51,843,804,338 samples, 1.46%) [.. [unknown] (49,115,074,635 samples, 1.38%) [.. [unknown] (47,020,328,627 samples, 1.32%) [unknown] (41,124,744,672 samples, 1.16%) [unknown] (38,571,784,780 samples, 1.08%) [unknown] (36,085,617,902 samples, 1.01%) [unknown] (32,172,048,607 samples, 0.90%) [unknown] (24,296,172,973 samples, 0.68%) [unknown] (14,033,556,774 samples, 0.39%) [unknown] (7,508,395,799 samples, 0.21%) [unknown] (3,295,574,070 samples, 0.09%) [unknown] (1,590,496,727 samples, 0.04%) [unknown] (1,002,849,637 samples, 0.03%) [unknown] (414,545,859 samples, 0.01%) libc.so.6::__lll_lock_wake_private (11,041,124,764 samples, 0.31%) [unknown] (10,991,162,572 samples, 0.31%) [unknown] (9,603,504,474 samples, 0.27%) [unknown] (9,459,439,012 samples, 0.27%) [unknown] (7,207,430,735 samples, 0.20%) [unknown] (5,830,933,319 samples, 0.16%) [unknown] (1,889,493,619 samples, 0.05%) [unknown] (394,342,984 samples, 0.01%) libc.so.6::_int_free (67,830,842,133 samples, 1.91%) li.. libc.so.6::_int_free_merge_chunk (832,998,780 samples, 0.02%) libc.so.6::cfree@GLIBC_2.2.5 (2,087,601,863 samples, 0.06%) libc.so.6::malloc_consolidate (3,954,686,383 samples, 0.11%) libc.so.6::unlink_chunk.isra.0 (497,585,449 samples, 0.01%) bitcoind::CRollingBloomFilter::insert (356,229,732 samples, 0.01%) [unknown] (444,029,098 samples, 0.01%) [unknown] (397,328,353 samples, 0.01%) [unknown] (397,328,353 samples, 0.01%) [unknown] (397,328,353 samples, 0.01%) [unknown] (397,328,353 samples, 0.01%) bitcoind::CRollingBloomFilter::insert (165,056,371,702 samples, 4.64%) bitcoind.. bitcoind::MurmurHash3 (79,485,956,130 samples, 2.24%) bit.. [unknown] (508,285,343 samples, 0.01%) [unknown] (450,228,615 samples, 0.01%) [unknown] (404,433,625 samples, 0.01%) [unknown] (404,433,625 samples, 0.01%) [unknown] (404,433,625 samples, 0.01%) [unknown] (404,433,625 samples, 0.01%) bitcoind::MurmurHash3 (5,783,718,949 samples, 0.16%) bitcoind::TxOrphanage::EraseForBlock (4,219,830,042 samples, 0.12%) bitcoind::std::_Rb_tree<COutPoint, std::pair<COutPoint const, std::set<std::_Rb_tree_iterator<std::pair<transaction_identifier<true> const, TxOrphanage::OrphanTx> >, TxOrphanage::IteratorComparator, std::allocator<std::_Rb_tree_iterator<std::pair<transaction_identifier<true> const, TxOrphanage::OrphanTx> > > > >, std::_Select1st<std::pair<COutPoint const, std::set<std::_Rb_tree_iterator<std::pair<transaction_identifier<true> const, TxOrphanage::OrphanTx> >, TxOrphanage::IteratorComparator, std::allocator<std::_Rb_tree_iterator<std::pair<transaction_identifier<true> const, TxOrphanage::OrphanTx> > > > > >, std::less<COutPoint>, std::allocator<std::pair<COutPoint const, std::set<std::_Rb_tree_iterator<std::pair<transaction_identifier<true> const, TxOrphanage::OrphanTx> >, TxOrphanage::IteratorComparator, std::allocator<std::_Rb_tree_iterator<std::pair<transaction_identifier<true> const, TxOrphanage::OrphanTx> > > > > > >::find (834,275,777 samples, 0.02%) bitcoind::node::TxDownloadManagerImpl::BlockConnected (176,131,189,628 samples, 4.95%) bitcoind:.. bitcoind::TxRequestTracker::ForgetTxHash (789,439,865 samples, 0.02%) bitcoind::std::_Function_handler<void (), ValidationSignals::BlockConnected(ChainstateRole, std::shared_ptr<CBlock const> const&, CBlockIndex const*)::{lambda()#2}>::_M_invoke (177,028,683,872 samples, 4.98%) bitcoind:.. bitcoind::std::_Sp_counted_ptr_inplace<CTransaction const, std::allocator<void>, (__gnu_cxx::_Lock_policy)2>::_M_dispose (18,094,676,466 samples, 0.51%) libc.so.6::cfree@GLIBC_2.2.5 (20,756,908,966 samples, 0.58%) bitcoind::std::_Sp_counted_ptr_inplace<CBlock, std::allocator<void>, (__gnu_cxx::_Lock_policy)2>::_M_dispose (48,020,349,476 samples, 1.35%) b.. libstdc++.so.6.0.32::operator delete (7,010,168,745 samples, 0.20%) bitcoind::std::_Sp_counted_ptr_inplace<CTransaction const, std::allocator<void>, (__gnu_cxx::_Lock_policy)2>::_M_dispose (595,474,492 samples, 0.02%) libc.so.6::cfree@GLIBC_2.2.5 (1,199,917,863 samples, 0.03%) bitcoind::std::_Function_handler<void (), ValidationSignals::BlockConnected(ChainstateRole, std::shared_ptr<CBlock const> const&, CBlockIndex const*)::{lambda()#2}>::_M_manager (50,738,017,178 samples, 1.43%) b.. bitcoind::std::_Sp_counted_base<(__gnu_cxx::_Lock_policy)2>::_M_release (50,738,017,178 samples, 1.43%) b.. libstdc++.so.6.0.32::operator delete (678,866,047 samples, 0.02%) bitcoind::CBlockPolicyEstimator::processBlock (2,721,087,031 samples, 0.08%) bitcoind::TxConfirmStats::UpdateMovingAverages (2,530,304,686 samples, 0.07%) bitcoind::std::_Function_handler<void (), ValidationSignals::MempoolTransactionsRemovedForBlock(std::vector<RemovedMempoolTransactionInfo, std::allocator<RemovedMempoolTransactionInfo> > const&, unsigned int)::{lambda()#2}>::_M_invoke (2,804,941,944 samples, 0.08%) bitcoind::SerialTaskRunner::ProcessQueue (230,828,220,555 samples, 6.49%) bitcoind::Se.. bitcoind::CScheduler::serviceQueue (231,341,597,555 samples, 6.51%) bitcoind::CS.. bitcoind::std::_Function_handler<void (), Repeat(CScheduler&, std::function<void ()>, std::chrono::duration<long, std::ratio<1l, 1000l> >)::{lambda()#1}>::_M_invoke (386,989,959 samples, 0.01%) bitcoind::Repeat (386,989,959 samples, 0.01%) bitcoind::CSHA512::Finalize (386,989,959 samples, 0.01%) b-scheduler (378,036,629,725 samples, 10.63%) b-scheduler libstdc++.so.6.0.32::execute_native_thread_routine (231,550,611,141 samples, 6.51%) libstdc++.so.. bitcoind::std::thread::_State_impl<std::thread::_Invoker<std::tuple<void (*)(std::basic_string_view<char, std::char_traits<char> >, std::function<void ()>), char const*, AppInitMain(node::NodeContext&, interfaces::BlockAndHeaderTipInfo*)::{lambda()#1}> > >::_M_run (231,550,611,141 samples, 6.51%) bitcoind::st.. bitcoind::util::TraceThread (231,550,611,141 samples, 6.51%) bitcoind::ut.. [unknown] (1,418,945,425 samples, 0.04%) [unknown] (1,418,945,425 samples, 0.04%) [unknown] (1,418,945,425 samples, 0.04%) [unknown] (1,418,945,425 samples, 0.04%) [unknown] (1,418,945,425 samples, 0.04%) [unknown] (1,418,945,425 samples, 0.04%) [unknown] (1,418,945,425 samples, 0.04%) [unknown] (1,368,549,335 samples, 0.04%) [unknown] (1,263,446,697 samples, 0.04%) [unknown] (1,105,228,005 samples, 0.03%) [unknown] (684,110,353 samples, 0.02%) [unknown] (1,463,102,999 samples, 0.04%) libc.so.6::_int_malloc (1,478,820,457 samples, 0.04%) [unknown] (1,323,329,878 samples, 0.04%) [unknown] (1,219,148,488 samples, 0.03%) [unknown] (1,167,736,581 samples, 0.03%) [unknown] (1,167,736,581 samples, 0.03%) [unknown] (1,115,451,061 samples, 0.03%) [unknown] (1,014,330,812 samples, 0.03%) [unknown] (911,337,057 samples, 0.03%) [unknown] (714,835,817 samples, 0.02%) [unknown] (456,457,319 samples, 0.01%) [unknown] (3,458,133,839 samples, 0.10%) bitcoind::CDBWrapper::~CDBWrapper (1,160,687,762 samples, 0.03%) bitcoind::leveldb::DBImpl::~DBImpl (1,160,687,762 samples, 0.03%) bitcoind::leveldb::DBImpl::~DBImpl (1,160,687,762 samples, 0.03%) bitcoind::leveldb::TableCache::~TableCache (1,160,687,762 samples, 0.03%) bitcoind::leveldb::(anonymous namespace)::ShardedLRUCache::~ShardedLRUCache (1,160,687,762 samples, 0.03%) bitcoind::leveldb::(anonymous namespace)::LRUCache::~LRUCache (1,160,687,762 samples, 0.03%) bitcoind::leveldb::DeleteEntry (1,160,687,762 samples, 0.03%) libc.so.6::__munmap (1,160,687,762 samples, 0.03%) [unknown] (1,160,687,762 samples, 0.03%) [unknown] (1,160,687,762 samples, 0.03%) [unknown] (1,160,687,762 samples, 0.03%) [unknown] (1,160,687,762 samples, 0.03%) [unknown] (1,160,687,762 samples, 0.03%) [unknown] (1,160,687,762 samples, 0.03%) [unknown] (1,160,687,762 samples, 0.03%) [unknown] (1,160,687,762 samples, 0.03%) [unknown] (1,160,687,762 samples, 0.03%) [unknown] (580,697,270 samples, 0.02%) bitcoind::leveldb::PutVarint32 (363,737,260 samples, 0.01%) bitcoind::leveldb::PutLengthPrefixedSlice (571,217,019 samples, 0.02%) bitcoind::leveldb::WriteBatch::Delete (2,702,574,018 samples, 0.08%) bitcoind::leveldb::WriteBatchInternal::SetCount (1,715,286,573 samples, 0.05%) bitcoind::leveldb::WriteBatchInternal::SetCount (1,453,616,163 samples, 0.04%) bitcoind::CDBBatch::EraseImpl (5,090,452,967 samples, 0.14%) bitcoind::leveldb::PutVarint32 (1,872,876,736 samples, 0.05%) bitcoind::leveldb::PutLengthPrefixedSlice (2,343,591,543 samples, 0.07%) bitcoind::leveldb::PutVarint32 (572,117,605 samples, 0.02%) bitcoind::leveldb::PutVarint32 (567,491,257 samples, 0.02%) bitcoind::leveldb::PutLengthPrefixedSlice (938,977,738 samples, 0.03%) bitcoind::leveldb::WriteBatchInternal::Count (619,405,896 samples, 0.02%) bitcoind::leveldb::WriteBatch::Put (2,689,024,451 samples, 0.08%) bitcoind::CDBBatch::WriteImpl (10,634,135,335 samples, 0.30%) bitcoind::leveldb::GetLengthPrefixedSlice (463,225,027 samples, 0.01%) bitcoind::leveldb::GetLengthPrefixedSlice (6,489,010,398 samples, 0.18%) bitcoind::leveldb::GetVarint32 (3,004,905,545 samples, 0.08%) bitcoind::leveldb::GetVarint32 (1,160,323,181 samples, 0.03%) bitcoind::leveldb::Arena::AllocateAligned (406,996,319 samples, 0.01%) bitcoind::leveldb::(anonymous namespace)::BytewiseComparatorImpl::Compare (717,398,174 samples, 0.02%) bitcoind::leveldb::MemTable::KeyComparator::operator (5,108,835,410 samples, 0.14%) bitcoind::leveldb::InternalKeyComparator::Compare (3,324,232,989 samples, 0.09%) bitcoind::leveldb::InternalKeyComparator::Compare (4,244,823,969 samples, 0.12%) bitcoind::leveldb::(anonymous namespace)::BytewiseComparatorImpl::Compare (3,179,677,931 samples, 0.09%) bitcoind::leveldb::(anonymous namespace)::BytewiseComparatorImpl::Compare (33,796,395,298 samples, 0.95%) bitcoind::memcmp@plt (943,665,852 samples, 0.03%) bitcoind::leveldb::SkipList<char const*, leveldb::MemTable::KeyComparator>::Insert (185,524,871,422 samples, 5.22%) bitcoind:.. bitcoind::leveldb::SkipList<char const*, leveldb::MemTable::KeyComparator>::FindGreaterOrEqual (178,286,921,652 samples, 5.01%) bitcoind:.. bitcoind::leveldb::MemTable::KeyComparator::operator (98,574,957,808 samples, 2.77%) bitc.. bitcoind::leveldb::InternalKeyComparator::Compare (75,114,665,063 samples, 2.11%) bi.. libc.so.6::__memcmp_evex_movbe (8,323,863,446 samples, 0.23%) bitcoind::leveldb::MemTable::Add (188,893,844,275 samples, 5.31%) bitcoind::.. bitcoind::leveldb::VarintLength (766,638,876 samples, 0.02%) bitcoind::leveldb::WriteBatchInternal::InsertInto (199,306,778,687 samples, 5.61%) bitcoind::.. bitcoind::leveldb::WriteBatch::Iterate (198,740,714,232 samples, 5.59%) bitcoind::.. bitcoind::crc32c::ExtendSse42 (471,197,509 samples, 0.01%) [[ext4]] (679,093,773 samples, 0.02%) [unknown] (522,409,669 samples, 0.01%) [[ext4]] (1,096,838,426 samples, 0.03%) [[ext4]] (1,722,362,275 samples, 0.05%) [unknown] (625,523,849 samples, 0.02%) [unknown] (574,147,567 samples, 0.02%) [unknown] (469,028,477 samples, 0.01%) [unknown] (469,028,477 samples, 0.01%) [unknown] (365,648,781 samples, 0.01%) [[ext4]] (4,389,086,262 samples, 0.12%) [unknown] (2,561,710,219 samples, 0.07%) [unknown] (2,561,710,219 samples, 0.07%) [unknown] (2,352,117,097 samples, 0.07%) [unknown] (1,880,182,821 samples, 0.05%) [unknown] (1,308,734,829 samples, 0.04%) [unknown] (523,736,031 samples, 0.01%) [[ext4]] (5,069,490,473 samples, 0.14%) [unknown] (5,069,490,473 samples, 0.14%) [unknown] (575,311,800 samples, 0.02%) [unknown] (470,084,210 samples, 0.01%) libc.so.6::__GI___libc_write (5,174,401,795 samples, 0.15%) [unknown] (5,174,401,795 samples, 0.15%) [unknown] (5,174,401,795 samples, 0.15%) [unknown] (5,174,401,795 samples, 0.15%) [unknown] (5,174,401,795 samples, 0.15%) bitcoind::CDBWrapper::WriteBatch (205,215,727,495 samples, 5.77%) bitcoind::C.. bitcoind::leveldb::DBImpl::Write (205,215,727,495 samples, 5.77%) bitcoind::l.. bitcoind::leveldb::log::Writer::AddRecord (5,908,948,808 samples, 0.17%) bitcoind::leveldb::log::Writer::EmitPhysicalRecord (5,908,948,808 samples, 0.17%) bitcoind::CompressScript (1,030,024,630 samples, 0.03%) bitcoind::prevector<33u, unsigned char, unsigned int, int>::resize (459,767,226 samples, 0.01%) bitcoind::void WriteVarInt<DataStream, (VarIntMode)0, unsigned int> (11,377,276,951 samples, 0.32%) bitcoind::void std::vector<std::byte, zero_after_free_allocator<std::byte> >::_M_range_insert<std::byte const*> (8,938,854,890 samples, 0.25%) bitcoind::CCoinsViewDB::BatchWrite (244,230,597,449 samples, 6.87%) bitcoind::CCo.. bitcoind::void std::vector<std::byte, zero_after_free_allocator<std::byte> >::_M_range_insert<std::byte const*> (6,482,431,215 samples, 0.18%) bitcoind::std::_Hashtable<COutPoint, std::pair<COutPoint const, CCoinsCacheEntry>, PoolAllocator<std::pair<COutPoint const, CCoinsCacheEntry>, 144ul, 8ul>, std::__detail::_Select1st, std::equal_to<COutPoint>, SaltedOutpointHasher, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<false, false, true> >::clear (13,761,064,935 samples, 0.39%) bitcoind::void std::vector<std::byte, zero_after_free_allocator<std::byte> >::_M_range_insert<std::byte const*> (364,557,178 samples, 0.01%) bitcoind::CCoinsViewCache::Flush (264,031,161,045 samples, 7.43%) bitcoind::CCoi.. libc.so.6::cfree@GLIBC_2.2.5 (5,262,867,110 samples, 0.15%) bitcoind::Chainstate::ForceFlushStateToDisk (264,186,830,154 samples, 7.43%) bitcoind::Chai.. bitcoind::Chainstate::FlushStateToDisk (264,186,830,154 samples, 7.43%) bitcoind::Chai.. libc.so.6::__libc_start_call_main (265,453,083,455 samples, 7.47%) libc.so.6::__l.. bitcoind::main (265,453,083,455 samples, 7.47%) bitcoind::main bitcoind::Shutdown (265,453,083,455 samples, 7.47%) bitcoind::Shut.. libc.so.6::_int_free (2,825,988,487 samples, 0.08%) libc.so.6::malloc_consolidate (2,950,349,980 samples, 0.08%) b-shutoff (278,389,331,208 samples, 7.83%) b-shutoff libc.so.6::unlink_chunk.isra.0 (3,181,018,445 samples, 0.09%) libc.so.6::_int_malloc (620,560,935 samples, 0.02%) [unknown] (518,649,070 samples, 0.01%) [unknown] (466,591,536 samples, 0.01%) [unknown] (466,591,536 samples, 0.01%) [unknown] (466,591,536 samples, 0.01%) [unknown] (415,625,450 samples, 0.01%) [unknown] (415,625,450 samples, 0.01%) [unknown] (363,215,208 samples, 0.01%) [unknown] (1,501,827,638 samples, 0.04%) bitcoind::leveldb::BlockBuilder::Add (581,064,351 samples, 0.02%) bitcoind::leveldb::TableBuilder::Add (1,003,488,869 samples, 0.03%) bitcoind::leveldb::DBImpl::WriteLevel0Table (1,214,913,728 samples, 0.03%) bitcoind::leveldb::BuildTable (1,214,913,728 samples, 0.03%) bitcoind::leveldb::WriteBatchInternal::InsertInto (2,528,384,688 samples, 0.07%) bitcoind::leveldb::WriteBatch::Iterate (2,528,384,688 samples, 0.07%) bitcoind::leveldb::MemTable::Add (2,422,985,691 samples, 0.07%) bitcoind::leveldb::SkipList<char const*, leveldb::MemTable::KeyComparator>::Insert (2,422,985,691 samples, 0.07%) bitcoind::leveldb::SkipList<char const*, leveldb::MemTable::KeyComparator>::FindGreaterOrEqual (2,318,036,540 samples, 0.07%) bitcoind::leveldb::MemTable::KeyComparator::operator (1,429,299,251 samples, 0.04%) bitcoind::leveldb::InternalKeyComparator::Compare (910,982,229 samples, 0.03%) bitcoind::CDBWrapper::CDBWrapper (5,007,147,537 samples, 0.14%) bitcoind::leveldb::DB::Open (5,007,147,537 samples, 0.14%) bitcoind::leveldb::DBImpl::Recover (4,954,666,055 samples, 0.14%) bitcoind::leveldb::DBImpl::RecoverLogFile (4,954,666,055 samples, 0.14%) libc.so.6::__memmove_avx512_unaligned_erms (1,000,227,273 samples, 0.03%) [unknown] (1,000,227,273 samples, 0.03%) [unknown] (1,000,227,273 samples, 0.03%) [unknown] (947,397,460 samples, 0.03%) [unknown] (947,397,460 samples, 0.03%) [unknown] (947,397,460 samples, 0.03%) [unknown] (841,684,608 samples, 0.02%) [unknown] (841,684,608 samples, 0.02%) [unknown] (841,684,608 samples, 0.02%) [unknown] (788,837,171 samples, 0.02%) bitcoind::node::BlockManager::GetAllBlockIndices (356,174,463 samples, 0.01%) bitcoind::base_uint<256u>::operator/= (4,353,340,184 samples, 0.12%) bitcoind::base_uint<256u>::operator>>=(unsigned int) (1,651,178,228 samples, 0.05%) bitcoind::GetBlockProof (4,611,529,418 samples, 0.13%) bitcoind::CSHA256::Finalize (469,172,416 samples, 0.01%) bitcoind::CSHA256::Write (416,395,152 samples, 0.01%) bitcoind::CBlockHeader::GetHash (889,433,319 samples, 0.03%) bitcoind::CSHA256::Write (420,260,903 samples, 0.01%) bitcoind::CheckProofOfWorkImpl (628,054,325 samples, 0.02%) bitcoind::arith_uint256::SetCompact (474,577,125 samples, 0.01%) bitcoind::leveldb::(anonymous namespace)::MergingIterator::Next (359,545,524 samples, 0.01%) bitcoind::leveldb::(anonymous namespace)::DBIter::FindNextUserEntry (615,884,661 samples, 0.02%) bitcoind::std::_Hashtable<uint256, std::pair<uint256 const, CBlockIndex>, std::allocator<std::pair<uint256 const, CBlockIndex> >, std::__detail::_Select1st, std::equal_to<uint256>, BlockHasher, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<true, false, true> >::_M_rehash (412,349,637 samples, 0.01%) bitcoind::node::BlockManager::InsertBlockIndex (926,266,820 samples, 0.03%) bitcoind::std::_Hashtable<uint256, std::pair<uint256 const, CBlockIndex>, std::allocator<std::pair<uint256 const, CBlockIndex> >, std::__detail::_Select1st, std::equal_to<uint256>, BlockHasher, std::__detail::_Mod_range_hashing, std::__detail::_Default_ranged_hash, std::__detail::_Prime_rehash_policy, std::__detail::_Hashtable_traits<true, false, true> >::_M_insert_unique_node (621,546,429 samples, 0.02%) bitcoind::kernel::BlockTreeDB::LoadBlockIndexGuts (4,371,914,721 samples, 0.12%) bitcoind::node::BlockManager::GetAllBlockIndices (360,699,633 samples, 0.01%) bitcoind::void std::__introsort_loop<__gnu_cxx::__normal_iterator<CBlockIndex**, std::vector<CBlockIndex*, std::allocator<CBlockIndex*> > >, long, __gnu_cxx::__ops::_Iter_comp_iter<node::CBlockIndexHeightOnlyComparator> > (487,613,426 samples, 0.01%) bitcoind::node::BlockManager::LoadBlockIndexDB (10,756,421,448 samples, 0.30%) bitcoind::node::BlockManager::LoadBlockIndex (10,397,563,911 samples, 0.29%) libc.so.6::__libc_start_call_main (17,915,410,780 samples, 0.50%) bitcoind::main (17,915,410,780 samples, 0.50%) bitcoind::AppInitMain (17,915,410,780 samples, 0.50%) bitcoind::InitAndLoadChainstate (17,915,410,780 samples, 0.50%) bitcoind::node::LoadChainstate (17,915,410,780 samples, 0.50%) bitcoind::node::CompleteChainstateInitialization (17,915,410,780 samples, 0.50%) bitcoind::ChainstateManager::LoadBlockIndex (12,499,349,673 samples, 0.35%) bitcoind::void std::__introsort_loop<__gnu_cxx::__normal_iterator<CBlockIndex**, std::vector<CBlockIndex*, std::allocator<CBlockIndex*> > >, long, __gnu_cxx::__ops::_Iter_comp_iter<node::CBlockIndexHeightOnlyComparator> > (711,414,524 samples, 0.02%) bitcoind::void std::__introsort_loop<__gnu_cxx::__normal_iterator<CBlockIndex**, std::vector<CBlockIndex*, std::allocator<CBlockIndex*> > >, long, __gnu_cxx::__ops::_Iter_comp_iter<node::CBlockIndexHeightOnlyComparator> > (401,238,745 samples, 0.01%) libc.so.6::_int_free (620,663,041 samples, 0.02%) bitcoind::leveldb::(anonymous namespace)::BytewiseComparatorImpl::Compare (868,925,227 samples, 0.02%) bitcoind::leveldb::(anonymous namespace)::MergingIterator::Valid (404,648,282 samples, 0.01%) bitcoind::leveldb::(anonymous namespace)::MergingIterator::value (764,967,422 samples, 0.02%) bitcoind::leveldb::Compaction::ShouldStopBefore (811,517,390 samples, 0.02%) bitcoind::leveldb::TableCache::Evict (924,140,736 samples, 0.03%) bitcoind::leveldb::(anonymous namespace)::ShardedLRUCache::Erase (924,140,736 samples, 0.03%) bitcoind::leveldb::(anonymous namespace)::LRUCache::FinishErase (924,140,736 samples, 0.03%) bitcoind::leveldb::(anonymous namespace)::LRUCache::Unref (924,140,736 samples, 0.03%) bitcoind::leveldb::DeleteEntry (924,140,736 samples, 0.03%) libc.so.6::__munmap (924,140,736 samples, 0.03%) [unknown] (924,140,736 samples, 0.03%) [unknown] (924,140,736 samples, 0.03%) [unknown] (924,140,736 samples, 0.03%) [unknown] (924,140,736 samples, 0.03%) [unknown] (924,140,736 samples, 0.03%) [unknown] (924,140,736 samples, 0.03%) [unknown] (924,140,736 samples, 0.03%) [unknown] (924,140,736 samples, 0.03%) [unknown] (924,140,736 samples, 0.03%) [unknown] (622,330,840 samples, 0.02%) [[jbd2]] (579,680,739 samples, 0.02%) bitcoind::leveldb::DBImpl::DeleteObsoleteFiles (5,386,961,700 samples, 0.15%) libc.so.6::__unlink (4,462,820,964 samples, 0.13%) [unknown] (4,462,820,964 samples, 0.13%) [unknown] (4,462,820,964 samples, 0.13%) [unknown] (4,462,820,964 samples, 0.13%) [unknown] (4,462,820,964 samples, 0.13%) [unknown] (4,462,820,964 samples, 0.13%) [[ext4]] (4,462,820,964 samples, 0.13%) [unknown] (4,413,928,808 samples, 0.12%) [unknown] (4,413,928,808 samples, 0.12%) [unknown] (3,629,480,214 samples, 0.10%) [unknown] (2,527,606,876 samples, 0.07%) [unknown] (1,289,801,972 samples, 0.04%) [unknown] (411,890,158 samples, 0.01%) bitcoind::leveldb::(anonymous namespace)::BytewiseComparatorImpl::Compare (1,451,370,022 samples, 0.04%) bitcoind::leveldb::(anonymous namespace)::BytewiseComparatorImpl::Compare (2,100,345,679 samples, 0.06%) bitcoind::leveldb::(anonymous namespace)::MergingIterator::FindSmallest (7,036,670,089 samples, 0.20%) bitcoind::leveldb::InternalKeyComparator::Compare (5,331,785,618 samples, 0.15%) libc.so.6::__memcmp_evex_movbe (467,739,292 samples, 0.01%) bitcoind::leveldb::Block::Iter::ParseNextKey (1,597,295,639 samples, 0.04%) bitcoind::leveldb::Block::Iter::key (719,412,755 samples, 0.02%) bitcoind::leveldb::(anonymous namespace)::TwoLevelIterator::Next (3,651,719,685 samples, 0.10%) [unknown] (775,514,001 samples, 0.02%) [unknown] (775,514,001 samples, 0.02%) [unknown] (775,514,001 samples, 0.02%) [unknown] (775,514,001 samples, 0.02%) [unknown] (723,468,265 samples, 0.02%) [unknown] (671,854,971 samples, 0.02%) [unknown] (620,745,631 samples, 0.02%) [unknown] (467,020,775 samples, 0.01%) bitcoind::leveldb::ReadBlock (5,036,746,240 samples, 0.14%) bitcoind::crc32c::ExtendSse42 (4,003,982,142 samples, 0.11%) bitcoind::leveldb::(anonymous namespace)::TwoLevelIterator::InitDataBlock (6,011,618,239 samples, 0.17%) bitcoind::leveldb::Table::BlockReader (5,654,181,527 samples, 0.16%) bitcoind::leveldb::(anonymous namespace)::TwoLevelIterator::SkipEmptyDataBlocksForward (6,370,810,843 samples, 0.18%) bitcoind::leveldb::Block::Iter::Valid (514,863,214 samples, 0.01%) bitcoind::leveldb::(anonymous namespace)::TwoLevelIterator::Next (11,878,686,935 samples, 0.33%) [unknown] (357,525,803 samples, 0.01%) bitcoind::leveldb::ReadBlock (1,021,671,534 samples, 0.03%) bitcoind::crc32c::ExtendSse42 (664,145,731 samples, 0.02%) bitcoind::leveldb::(anonymous namespace)::TwoLevelIterator::InitDataBlock (1,177,165,099 samples, 0.03%) bitcoind::leveldb::Table::BlockReader (1,073,054,446 samples, 0.03%) bitcoind::leveldb::(anonymous namespace)::TwoLevelIterator::SkipEmptyDataBlocksForward (3,944,657,665 samples, 0.11%) bitcoind::leveldb::(anonymous namespace)::TwoLevelIterator::Valid (468,344,432 samples, 0.01%) bitcoind::leveldb::Block::Iter::Valid (360,443,695 samples, 0.01%) bitcoind::leveldb::(anonymous namespace)::MergingIterator::Next (25,840,019,062 samples, 0.73%) bitcoind::leveldb::InternalKeyComparator::Compare (877,755,927 samples, 0.02%) bitcoind::leveldb::(anonymous namespace)::MergingIterator::value (460,966,118 samples, 0.01%) bitcoind::leveldb::(anonymous namespace)::TwoLevelIterator::value (1,168,083,499 samples, 0.03%) bitcoind::leveldb::(anonymous namespace)::BytewiseComparatorImpl::Compare (1,027,703,208 samples, 0.03%) bitcoind::leveldb::Compaction::IsBaseLevelForKey (3,331,453,084 samples, 0.09%) libc.so.6::__memcmp_evex_movbe (1,380,364,868 samples, 0.04%) bitcoind::leveldb::(anonymous namespace)::BytewiseComparatorImpl::Compare (1,428,625,135 samples, 0.04%) bitcoind::leveldb::Compaction::ShouldStopBefore (5,019,787,360 samples, 0.14%) bitcoind::leveldb::InternalKeyComparator::Compare (3,376,359,370 samples, 0.09%) libc.so.6::__memcmp_evex_movbe (1,229,056,330 samples, 0.03%) bitcoind::leveldb::DBImpl::DeleteObsoleteFiles (947,024,277 samples, 0.03%) libc.so.6::__unlink (947,024,277 samples, 0.03%) [unknown] (947,024,277 samples, 0.03%) [unknown] (947,024,277 samples, 0.03%) [unknown] (947,024,277 samples, 0.03%) [unknown] (947,024,277 samples, 0.03%) [unknown] (947,024,277 samples, 0.03%) [[ext4]] (947,024,277 samples, 0.03%) [unknown] (947,024,277 samples, 0.03%) [unknown] (947,024,277 samples, 0.03%) [unknown] (891,341,341 samples, 0.03%) [unknown] (632,138,490 samples, 0.02%) [unknown] (416,723,130 samples, 0.01%) bitcoind::leveldb::MemTableIterator::key (1,087,232,643 samples, 0.03%) bitcoind::leveldb::(anonymous namespace)::BytewiseComparatorImpl::Compare (7,618,678,897 samples, 0.21%) bitcoind::leveldb::(anonymous namespace)::BytewiseComparatorImpl::Compare (3,626,299,706 samples, 0.10%) bitcoind::leveldb::InternalKeyComparator::Compare (5,542,426,443 samples, 0.16%) bitcoind::leveldb::PutVarint32 (983,352,417 samples, 0.03%) bitcoind::leveldb::EncodeVarint32 (516,604,326 samples, 0.01%) bitcoind::leveldb::BlockBuilder::Add (15,702,002,539 samples, 0.44%) bitcoind::leveldb::FilterBlockBuilder::AddKey (412,090,761 samples, 0.01%) bitcoind::leveldb::(anonymous namespace)::BloomFilterPolicy::CreateFilter (2,066,056,339 samples, 0.06%) bitcoind::leveldb::Hash (665,922,831 samples, 0.02%) bitcoind::leveldb::FilterBlockBuilder::StartBlock (2,377,127,267 samples, 0.07%) bitcoind::leveldb::FilterBlockBuilder::GenerateFilter (2,377,127,267 samples, 0.07%) bitcoind::leveldb::InternalKeyComparator::Compare (774,664,618 samples, 0.02%) [[ext4]] (567,830,671 samples, 0.02%) [[ext4]] (929,740,986 samples, 0.03%) [unknown] (361,910,315 samples, 0.01%) [[ext4]] (3,216,007,087 samples, 0.09%) [unknown] (2,077,722,358 samples, 0.06%) [unknown] (2,025,638,088 samples, 0.06%) [unknown] (1,766,421,841 samples, 0.05%) [unknown] (1,349,297,830 samples, 0.04%) [unknown] (985,540,031 samples, 0.03%) [[ext4]] (4,245,378,964 samples, 0.12%) [unknown] (4,245,378,964 samples, 0.12%) [unknown] (821,567,389 samples, 0.02%) bitcoind::leveldb::TableBuilder::Flush (5,177,109,910 samples, 0.15%) libc.so.6::__GI___libc_write (4,762,579,653 samples, 0.13%) [unknown] (4,762,579,653 samples, 0.13%) [unknown] (4,762,579,653 samples, 0.13%) [unknown] (4,762,579,653 samples, 0.13%) [unknown] (4,607,316,631 samples, 0.13%) libc.so.6::__memcmp_evex_movbe (2,327,620,616 samples, 0.07%) bitcoind::leveldb::TableBuilder::Add (29,098,360,859 samples, 0.82%) libc.so.6::__memmove_avx512_unaligned_erms (880,376,005 samples, 0.02%) [[ext4]] (576,641,035 samples, 0.02%) [unknown] (419,245,830 samples, 0.01%) [[ext4]] (681,686,302 samples, 0.02%) [[ext4]] (886,736,982 samples, 0.02%) [[ext4]] (886,736,982 samples, 0.02%) [[ext4]] (886,736,982 samples, 0.02%) bitcoind::leveldb::BuildTable (31,594,879,610 samples, 0.89%) libc.so.6::fdatasync (990,237,376 samples, 0.03%) [unknown] (990,237,376 samples, 0.03%) [unknown] (990,237,376 samples, 0.03%) [unknown] (990,237,376 samples, 0.03%) [[ext4]] (990,237,376 samples, 0.03%) [unknown] (990,237,376 samples, 0.03%) [unknown] (990,237,376 samples, 0.03%) [unknown] (990,237,376 samples, 0.03%) [unknown] (990,237,376 samples, 0.03%) bitcoind::leveldb::DBImpl::CompactMemTable (32,644,397,020 samples, 0.92%) bitcoind::leveldb::DBImpl::WriteLevel0Table (31,697,372,743 samples, 0.89%) [[ext4]] (360,420,776 samples, 0.01%) bitcoind::leveldb::TableBuilder::Finish (565,702,739 samples, 0.02%) bitcoind::leveldb::TableBuilder::WriteRawBlock (411,712,919 samples, 0.01%) libc.so.6::__GI___libc_write (411,712,919 samples, 0.01%) [unknown] (411,712,919 samples, 0.01%) [unknown] (411,712,919 samples, 0.01%) [unknown] (411,712,919 samples, 0.01%) [unknown] (411,712,919 samples, 0.01%) [[ext4]] (411,712,919 samples, 0.01%) [unknown] (411,712,919 samples, 0.01%) [[ext4]] (2,407,378,967 samples, 0.07%) [unknown] (1,896,402,811 samples, 0.05%) [unknown] (1,223,588,483 samples, 0.03%) [unknown] (359,102,837 samples, 0.01%) [[ext4]] (3,282,391,421 samples, 0.09%) [unknown] (669,408,205 samples, 0.02%) [[nvme]] (410,427,902 samples, 0.01%) [[nvme]] (410,427,902 samples, 0.01%) [unknown] (410,427,902 samples, 0.01%) [unknown] (410,427,902 samples, 0.01%) [[ext4]] (5,846,551,102 samples, 0.16%) [unknown] (1,539,251,741 samples, 0.04%) [unknown] (1,332,958,992 samples, 0.04%) [unknown] (1,230,554,197 samples, 0.03%) [unknown] (1,230,554,197 samples, 0.03%) [unknown] (1,230,554,197 samples, 0.03%) [unknown] (410,593,098 samples, 0.01%) [[ext4]] (5,999,990,575 samples, 0.17%) [[ext4]] (5,999,990,575 samples, 0.17%) bitcoind::leveldb::DBImpl::FinishCompactionOutputFile (7,646,287,561 samples, 0.22%) libc.so.6::fdatasync (6,926,959,748 samples, 0.19%) [unknown] (6,926,959,748 samples, 0.19%) [unknown] (6,926,959,748 samples, 0.19%) [unknown] (6,926,959,748 samples, 0.19%) [[ext4]] (6,926,959,748 samples, 0.19%) [unknown] (6,926,959,748 samples, 0.19%) [unknown] (6,926,959,748 samples, 0.19%) [unknown] (6,926,959,748 samples, 0.19%) [unknown] (6,720,904,548 samples, 0.19%) [unknown] (618,546,651 samples, 0.02%) [unknown] (618,541,816 samples, 0.02%) [unknown] (618,541,816 samples, 0.02%) [unknown] (618,541,816 samples, 0.02%) [unknown] (513,838,124 samples, 0.01%) [unknown] (411,261,494 samples, 0.01%) bitcoind::leveldb::InternalKeyComparator::Compare (1,176,215,358 samples, 0.03%) bitcoind::leveldb::(anonymous namespace)::BytewiseComparatorImpl::Compare (1,597,987,748 samples, 0.04%) bitcoind::leveldb::EncodeVarint32 (667,083,479 samples, 0.02%) bitcoind::leveldb::(anonymous namespace)::BytewiseComparatorImpl::Compare (3,023,925,193 samples, 0.09%) bitcoind::leveldb::InternalKeyComparator::Compare (5,485,200,607 samples, 0.15%) libc.so.6::__memcmp_evex_movbe (768,462,744 samples, 0.02%) bitcoind::leveldb::BlockBuilder::Add (19,355,464,658 samples, 0.54%) bitcoind::leveldb::PutVarint32 (3,963,072,776 samples, 0.11%) bitcoind::leveldb::EncodeVarint32 (2,006,933,285 samples, 0.06%) bitcoind::leveldb::FilterBlockBuilder::AddKey (1,861,448,821 samples, 0.05%) bitcoind::leveldb::(anonymous namespace)::BloomFilterPolicy::CreateFilter (13,758,298,035 samples, 0.39%) bitcoind::leveldb::Hash (5,062,387,301 samples, 0.14%) bitcoind::leveldb::InternalFilterPolicy::CreateFilter (408,507,196 samples, 0.01%) bitcoind::std::vector<leveldb::Slice, std::allocator<leveldb::Slice> >::_M_default_append (1,029,970,476 samples, 0.03%) bitcoind::leveldb::FilterBlockBuilder::GenerateFilter (16,416,940,319 samples, 0.46%) bitcoind::leveldb::FilterBlockBuilder::StartBlock (16,468,035,714 samples, 0.46%) bitcoind::leveldb::(anonymous namespace)::BytewiseComparatorImpl::Compare (1,040,462,681 samples, 0.03%) bitcoind::leveldb::InternalKeyComparator::Compare (2,329,875,977 samples, 0.07%) bitcoind::leveldb::InternalKeyComparator::FindShortestSeparator (358,735,789 samples, 0.01%) bitcoind::leveldb::PutVarint32 (610,801,466 samples, 0.02%) bitcoind::crc32c::ExtendSse42 (874,382,210 samples, 0.02%) bitcoind::leveldb::TableBuilder::WriteBlock (1,806,524,733 samples, 0.05%) bitcoind::leveldb::TableBuilder::WriteRawBlock (1,390,163,236 samples, 0.04%) libc.so.6::__memmove_avx512_unaligned_erms (413,005,584 samples, 0.01%) [[ext4]] (720,896,427 samples, 0.02%) [[ext4]] (2,836,852,977 samples, 0.08%) [unknown] (1,356,279,497 samples, 0.04%) [[ext4]] (3,876,087,820 samples, 0.11%) [unknown] (634,447,162 samples, 0.02%) [[ext4]] (6,595,884,839 samples, 0.19%) [unknown] (2,409,927,037 samples, 0.07%) [unknown] (2,152,146,763 samples, 0.06%) [unknown] (1,946,544,284 samples, 0.05%) [unknown] (1,691,057,617 samples, 0.05%) [unknown] (1,332,315,567 samples, 0.04%) [unknown] (618,194,201 samples, 0.02%) [unknown] (411,783,313 samples, 0.01%) [[ext4]] (21,402,165,352 samples, 0.60%) [unknown] (13,825,328,165 samples, 0.39%) [unknown] (12,948,506,018 samples, 0.36%) [unknown] (10,591,496,268 samples, 0.30%) [unknown] (8,635,293,060 samples, 0.24%) [unknown] (5,512,816,463 samples, 0.16%) [unknown] (1,755,230,935 samples, 0.05%) [unknown] (358,610,982 samples, 0.01%) [[ext4]] (26,848,872,865 samples, 0.76%) [unknown] (26,183,441,807 samples, 0.74%) [unknown] (3,805,768,350 samples, 0.11%) [unknown] (2,522,380,066 samples, 0.07%) libc.so.6::__GI___libc_write (29,870,807,469 samples, 0.84%) [unknown] (29,663,737,328 samples, 0.83%) [unknown] (29,456,391,053 samples, 0.83%) [unknown] (29,306,607,963 samples, 0.82%) [unknown] (28,793,621,717 samples, 0.81%) [unknown] (869,287,921 samples, 0.02%) bitcoind::leveldb::TableBuilder::Flush (32,039,566,359 samples, 0.90%) bitcoind::leveldb::TableBuilder::status (2,416,608,293 samples, 0.07%) bitcoind::memcpy@plt (1,533,086,169 samples, 0.04%) libc.so.6::__memcmp_evex_movbe (11,663,095,994 samples, 0.33%) libc.so.6::__memmove_avx512_unaligned_erms (6,084,682,703 samples, 0.17%) bitcoind::leveldb::TableBuilder::Add (101,316,031,082 samples, 2.85%) bitc.. bitcoind::leveldb::TableBuilder::NumEntries (460,667,349 samples, 0.01%) libc.so.6::__memcmp_evex_movbe (359,824,779 samples, 0.01%) bitcoind::leveldb::DBImpl::DoCompactionWork (188,768,693,249 samples, 5.31%) bitcoind:.. libc.so.6::__memmove_avx512_unaligned_erms (972,425,560 samples, 0.03%) bitcoind::leveldb::TableBuilder::NumEntries (767,314,029 samples, 0.02%) bitcoind::leveldb::DBImpl::BackgroundCompaction (198,697,568,504 samples, 5.59%) bitcoind::.. libc.so.6::__memmove_avx512_unaligned_erms (569,144,596 samples, 0.02%) bitcoind::leveldb::DBImpl::DeleteObsoleteFiles (591,819,871 samples, 0.02%) libc.so.6::__unlink (591,819,871 samples, 0.02%) [unknown] (591,819,871 samples, 0.02%) [unknown] (591,819,871 samples, 0.02%) [unknown] (591,819,871 samples, 0.02%) [unknown] (591,819,871 samples, 0.02%) [unknown] (591,819,871 samples, 0.02%) [[ext4]] (591,819,871 samples, 0.02%) [unknown] (591,819,871 samples, 0.02%) [unknown] (591,819,871 samples, 0.02%) [unknown] (479,954,726 samples, 0.01%) [unknown] (428,868,095 samples, 0.01%) bitcoind::leveldb::(anonymous namespace)::BytewiseComparatorImpl::Compare (4,760,717,074 samples, 0.13%) bitcoind::leveldb::(anonymous namespace)::BytewiseComparatorImpl::Compare (1,338,954,347 samples, 0.04%) bitcoind::leveldb::InternalKeyComparator::Compare (2,113,914,207 samples, 0.06%) bitcoind::leveldb::BlockBuilder::Add (8,483,080,141 samples, 0.24%) bitcoind::leveldb::PutVarint32 (468,110,226 samples, 0.01%) bitcoind::leveldb::(anonymous namespace)::BloomFilterPolicy::CreateFilter (981,556,026 samples, 0.03%) bitcoind::leveldb::Hash (364,078,664 samples, 0.01%) bitcoind::leveldb::FilterBlockBuilder::StartBlock (1,085,605,353 samples, 0.03%) bitcoind::leveldb::FilterBlockBuilder::GenerateFilter (1,085,605,353 samples, 0.03%) [[ext4]] (363,216,075 samples, 0.01%) [[ext4]] (414,434,148 samples, 0.01%) [[ext4]] (622,462,403 samples, 0.02%) [[ext4]] (2,219,690,360 samples, 0.06%) [unknown] (1,545,848,943 samples, 0.04%) [unknown] (1,545,848,943 samples, 0.04%) [unknown] (1,344,249,592 samples, 0.04%) [unknown] (1,034,709,836 samples, 0.03%) [unknown] (463,122,475 samples, 0.01%) [[ext4]] (2,730,864,687 samples, 0.08%) [unknown] (2,627,509,960 samples, 0.07%) bitcoind::leveldb::TableBuilder::Flush (2,941,422,377 samples, 0.08%) libc.so.6::__GI___libc_write (2,889,358,538 samples, 0.08%) [unknown] (2,889,358,538 samples, 0.08%) [unknown] (2,837,160,085 samples, 0.08%) [unknown] (2,837,160,085 samples, 0.08%) [unknown] (2,837,160,085 samples, 0.08%) libc.so.6::__memcmp_evex_movbe (870,026,684 samples, 0.02%) bitcoind::leveldb::TableBuilder::Add (14,671,945,001 samples, 0.41%) libc.so.6::__memmove_avx512_unaligned_erms (516,334,186 samples, 0.01%) [[ext4]] (366,285,823 samples, 0.01%) bitcoind::leveldb::BuildTable (15,764,968,843 samples, 0.44%) libc.so.6::fdatasync (522,804,809 samples, 0.01%) [unknown] (522,804,809 samples, 0.01%) [unknown] (522,804,809 samples, 0.01%) [unknown] (522,804,809 samples, 0.01%) [[ext4]] (522,804,809 samples, 0.01%) [unknown] (522,804,809 samples, 0.01%) [unknown] (522,804,809 samples, 0.01%) [unknown] (522,804,809 samples, 0.01%) [unknown] (522,804,809 samples, 0.01%) [[ext4]] (470,717,222 samples, 0.01%) [[ext4]] (470,717,222 samples, 0.01%) [[ext4]] (470,717,222 samples, 0.01%) libstdc++.so.6.0.32::execute_native_thread_routine (215,158,735,915 samples, 6.05%) libstdc++.s.. bitcoind::leveldb::(anonymous namespace)::PosixEnv::BackgroundThreadEntryPoint (215,158,735,915 samples, 6.05%) bitcoind::l.. bitcoind::leveldb::DBImpl::BackgroundCall (215,158,735,915 samples, 6.05%) bitcoind::l.. bitcoind::leveldb::DBImpl::CompactMemTable (16,461,167,411 samples, 0.46%) bitcoind::leveldb::DBImpl::WriteLevel0Table (15,869,347,540 samples, 0.45%) bitcoind (236,278,709,104 samples, 6.65%) bitcoind all (3,555,551,407,309 samples, 100%)
diff --git a/flake.lock b/flake.lock
new file mode 100644
index 000000000000..fc1308c520fa
--- /dev/null
+++ b/flake.lock
@@ -0,0 +1,27 @@
+{
+ "nodes": {
+ "nixpkgs": {
+ "locked": {
+ "lastModified": 1764983851,
+ "narHash": "sha256-y7RPKl/jJ/KAP/VKLMghMgXTlvNIJMHKskl8/Uuar7o=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "d9bc5c7dceb30d8d6fafa10aeb6aa8a48c218454",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixos-25.11",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "root": {
+ "inputs": {
+ "nixpkgs": "nixpkgs"
+ }
+ }
+ },
+ "root": "root",
+ "version": 7
+}
diff --git a/flake.nix b/flake.nix
new file mode 100644
index 000000000000..b42180629d1a
--- /dev/null
+++ b/flake.nix
@@ -0,0 +1,170 @@
+{
+ description = "bitcoind for benchmarking";
+
+ inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
+
+ outputs =
+ { self, nixpkgs }:
+ let
+ systems = [
+ "x86_64-linux"
+ "aarch64-darwin"
+ ];
+
+ forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f system);
+
+ pkgsFor = system: import nixpkgs { inherit system; };
+
+ mkBitcoinCore =
+ system:
+ let
+ pkgs = pkgsFor system;
+ inherit (pkgs) lib;
+
+ pname = "bitcoin-core";
+ version = self.shortRev or "dirty";
+
+ CFlags = toString [
+ "-O2"
+ "-g"
+ ];
+ CXXFlags = "${CFlags} -fno-omit-frame-pointer";
+
+ nativeBuildInputs = [
+ pkgs.cmake
+ pkgs.ninja
+ pkgs.pkg-config
+ pkgs.python3
+ ];
+
+ buildInputs = [
+ pkgs.boost188.dev
+ pkgs.libevent.dev
+ ];
+
+ cmakeFlags = [
+ "-DBUILD_BENCH=OFF"
+ "-DBUILD_BITCOIN_BIN=OFF"
+ "-DBUILD_CLI=OFF"
+ "-DBUILD_DAEMON=ON"
+ "-DBUILD_FUZZ_BINARY=OFF"
+ "-DBUILD_GUI_TESTS=OFF"
+ "-DBUILD_TESTS=OFF"
+ "-DBUILD_TX=OFF"
+ "-DBUILD_UTIL=OFF"
+ "-DBUILD_WALLET_TOOL=OFF"
+ "-DCMAKE_BUILD_TYPE=RelWithDebInfo"
+ "-DCMAKE_SKIP_RPATH=ON"
+ "-DENABLE_EXTERNAL_SIGNER=OFF"
+ "-DENABLE_IPC=OFF"
+ "-DENABLE_WALLET=OFF"
+ "-DREDUCE_EXPORTS=ON"
+ "-DWITH_ZMQ=OFF"
+ ];
+ in
+ pkgs.stdenv.mkDerivation {
+ inherit
+ pname
+ version
+ nativeBuildInputs
+ buildInputs
+ cmakeFlags
+ ;
+
+ preConfigure = ''
+ cmakeFlagsArray+=(
+ "-DAPPEND_CFLAGS=${CFlags}"
+ "-DAPPEND_CXXFLAGS=${CXXFlags}"
+ "-DAPPEND_LDFLAGS=-Wl,--as-needed -Wl,-O2"
+ )
+ '';
+
+ src = builtins.path {
+ path = ./.;
+ name = "source";
+ };
+
+ env = {
+ CMAKE_GENERATOR = "Ninja";
+ LC_ALL = "C";
+ LIBRARY_PATH = "";
+ CPATH = "";
+ C_INCLUDE_PATH = "";
+ CPLUS_INCLUDE_PATH = "";
+ OBJC_INCLUDE_PATH = "";
+ OBJCPLUS_INCLUDE_PATH = "";
+ };
+
+ dontStrip = true;
+
+ meta = {
+ description = "bitcoind for benchmarking";
+ homepage = "https://bitcoincore.org/";
+ license = lib.licenses.mit;
+ };
+ };
+ in
+ {
+ packages = forAllSystems (system: {
+ default = mkBitcoinCore system;
+ });
+
+ formatter = forAllSystems (system: (pkgsFor system).nixfmt-tree);
+
+ devShells = forAllSystems (
+ system:
+ let
+ pkgs = pkgsFor system;
+ inherit (pkgs) stdenv;
+
+ # Override the default cargo-flamegraph with a custom fork including bitcoin highlighting
+ cargo-flamegraph = pkgs.rustPlatform.buildRustPackage rec {
+ pname = "flamegraph";
+ version = "bitcoin-core";
+
+ src = pkgs.fetchFromGitHub {
+ owner = "willcl-ark";
+ repo = "flamegraph";
+ rev = "bitcoin-core";
+ sha256 = "sha256-tQbr3MYfAiOxeT12V9au5KQK5X5JeGuV6p8GR/Sgen4=";
+ };
+
+ doCheck = false;
+ cargoHash = "sha256-QWPqTyTFSZNJNayNqLmsQSu0rX26XBKfdLROZ9tRjrg=";
+
+ nativeBuildInputs = pkgs.lib.optionals stdenv.hostPlatform.isLinux [ pkgs.makeWrapper ];
+ buildInputs = pkgs.lib.optionals stdenv.hostPlatform.isDarwin [
+ pkgs.darwin.apple_sdk.frameworks.Security
+ ];
+
+ postFixup = pkgs.lib.optionalString stdenv.hostPlatform.isLinux ''
+ wrapProgram $out/bin/cargo-flamegraph \
+ --set-default PERF ${pkgs.perf}/bin/perf
+ wrapProgram $out/bin/flamegraph \
+ --set-default PERF ${pkgs.perf}/bin/perf
+ '';
+ };
+ in
+ {
+ default = pkgs.mkShell {
+ buildInputs = [
+ # Benchmarking
+ cargo-flamegraph
+ pkgs.flamegraph
+ pkgs.hyperfine
+ pkgs.jq
+ pkgs.just
+ pkgs.perf
+ pkgs.perf-tools
+ pkgs.python312
+ pkgs.python312Packages.matplotlib
+ pkgs.util-linux
+
+ # Binary patching
+ pkgs.patchelf
+ ];
+ };
+ }
+ );
+ };
+}
diff --git a/justfile b/justfile
new file mode 100644
index 000000000000..d128c7e8b195
--- /dev/null
+++ b/justfile
@@ -0,0 +1,115 @@
+set shell := ["bash", "-uc"]
+
+default:
+ just --list
+
+# ============================================================================
+# Local benchmarking commands
+# ============================================================================
+
+# Test instrumented run using signet (includes report generation)
+[group('local')]
+test-instrumented base head datadir:
+ nix develop --command python3 bench.py build --skip-existing {{ base }}:base {{ head }}:head
+ nix develop --command python3 bench.py --profile quick run \
+ --chain signet \
+ --instrumented \
+ --datadir {{ datadir }} \
+ base:./binaries/base/bitcoind \
+ head:./binaries/head/bitcoind
+ nix develop --command python3 bench.py report bench-output/ bench-output/
+
+# Test uninstrumented run using signet
+[group('local')]
+test-uninstrumented base head datadir:
+ nix develop --command python3 bench.py build --skip-existing {{ base }}:base {{ head }}:head
+ nix develop --command python3 bench.py --profile quick run \
+ --chain signet \
+ --datadir {{ datadir }} \
+ base:./binaries/base/bitcoind \
+ head:./binaries/head/bitcoind
+
+# Full benchmark with instrumentation (flamegraphs + plots)
+[group('local')]
+instrumented base head datadir:
+ python3 bench.py build {{ base }}:base {{ head }}:head
+ python3 bench.py --profile quick run \
+ --instrumented \
+ --datadir {{ datadir }} \
+ base:./binaries/base/bitcoind \
+ head:./binaries/head/bitcoind
+
+# Just build binaries (useful for incremental testing)
+[group('local')]
+build *commits:
+ python3 bench.py build {{ commits }}
+
+# Run benchmark with pre-built binaries
+[group('local')]
+run datadir *binaries:
+ python3 bench.py run --datadir {{ datadir }} {{ binaries }}
+
+# Generate plots from a debug.log file
+[group('local')]
+analyze commit logfile output_dir="./plots":
+ python3 bench.py analyze {{ commit }} {{ logfile }} --output-dir {{ output_dir }}
+
+# Compare benchmark results
+[group('local')]
+compare *results_files:
+ python3 bench.py compare {{ results_files }}
+
+# Generate HTML report from benchmark results
+[group('local')]
+report input_dir output_dir:
+ python3 bench.py report {{ input_dir }} {{ output_dir }}
+
+# ============================================================================
+# CI commands (called by GitHub Actions)
+# ============================================================================
+
+# Build binaries for CI
+[group('ci')]
+ci-build base_commit head_commit binaries_dir:
+ python3 bench.py build -o {{ binaries_dir }} {{ base_commit }}:base {{ head_commit }}:head
+
+# Run uninstrumented benchmarks for CI
+[group('ci')]
+ci-run datadir tmp_datadir output_dir dbcache binaries_dir:
+ python3 bench.py --profile ci run \
+ --datadir {{ datadir }} \
+ --tmp-datadir {{ tmp_datadir }} \
+ --output-dir {{ output_dir }} \
+ --dbcache {{ dbcache }} \
+ base:{{ binaries_dir }}/base/bitcoind \
+ head:{{ binaries_dir }}/head/bitcoind
+
+# Run instrumented benchmarks for CI
+[group('ci')]
+ci-run-instrumented datadir tmp_datadir output_dir dbcache binaries_dir:
+ python3 bench.py --profile ci run \
+ --instrumented \
+ --datadir {{ datadir }} \
+ --tmp-datadir {{ tmp_datadir }} \
+ --output-dir {{ output_dir }} \
+ --dbcache {{ dbcache }} \
+ base:{{ binaries_dir }}/base/bitcoind \
+ head:{{ binaries_dir }}/head/bitcoind
+
+# ============================================================================
+# Git helpers
+# ============================================================================
+
+# Cherry-pick commits from a Bitcoin Core PR onto this branch
+[group('git')]
+pick-pr pr_number:
+ #!/usr/bin/env bash
+ set -euxo pipefail
+
+ if ! git remote get-url upstream 2>/dev/null | grep -q "bitcoin/bitcoin"; then
+ echo "Error: 'upstream' remote not found or doesn't point to bitcoin/bitcoin"
+ echo "Please add it with: git remote add upstream https://github.com/bitcoin/bitcoin.git"
+ exit 1
+ fi
+
+ git fetch upstream pull/{{ pr_number }}/head:bench-{{ pr_number }} && git cherry-pick $(git rev-list --reverse bench-{{ pr_number }} --not upstream/master)
diff --git a/src/bench/CMakeLists.txt b/src/bench/CMakeLists.txt
index e0e03b1df7cc..c82f48b6af0a 100644
--- a/src/bench/CMakeLists.txt
+++ b/src/bench/CMakeLists.txt
@@ -19,6 +19,7 @@ add_executable(bench_bitcoin
checkblockindex.cpp
checkqueue.cpp
cluster_linearize.cpp
+ coinsviewcacheasync.cpp
connectblock.cpp
crypto_hash.cpp
descriptors.cpp
diff --git a/src/bench/coinsviewcacheasync.cpp b/src/bench/coinsviewcacheasync.cpp
new file mode 100644
index 000000000000..9d8fdcf703c5
--- /dev/null
+++ b/src/bench/coinsviewcacheasync.cpp
@@ -0,0 +1,53 @@
+// Copyright (c) The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+
+static void CoinsViewCacheAsyncBenchmark(benchmark::Bench& bench)
+{
+ CBlock block;
+ DataStream{benchmark::data::block413567} >> TX_WITH_WITNESS(block);
+ const auto testing_setup{MakeNoLogFileContext(ChainType::MAIN, { .coins_db_in_memory = false })};
+ Chainstate& chainstate{testing_setup->m_node.chainman->ActiveChainstate()};
+ auto& coins_tip{WITH_LOCK(testing_setup->m_node.chainman->GetMutex(), return chainstate.CoinsTip();)};
+
+ for (const auto& tx : block.vtx | std::views::drop(1)) {
+ for (const auto& in : tx->vin) {
+ Coin coin{};
+ coin.out.nValue = 1;
+ coins_tip.EmplaceCoinInternalDANGER(COutPoint{in.prevout}, std::move(coin));
+ }
+ }
+ chainstate.ForceFlushStateToDisk();
+ const auto& coins_db{WITH_LOCK(testing_setup->m_node.chainman->GetMutex(), return chainstate.CoinsDB();)};
+ CoinsViewCacheAsync async_cache{coins_tip, coins_db};
+
+ bench.run([&] {
+ async_cache.StartFetching(block);
+ for (const auto& tx : block.vtx | std::views::drop(1)) {
+ for (const auto& in : tx->vin) {
+ const auto have{async_cache.HaveCoin(in.prevout)};
+ assert(have);
+ }
+ }
+ async_cache.Reset();
+ });
+}
+
+BENCHMARK(CoinsViewCacheAsyncBenchmark, benchmark::PriorityLevel::HIGH);
diff --git a/src/coins.cpp b/src/coins.cpp
index 554a3ebe962b..d7bd45fb28a6 100644
--- a/src/coins.cpp
+++ b/src/coins.cpp
@@ -173,6 +173,12 @@ bool CCoinsViewCache::HaveCoinInCache(const COutPoint &outpoint) const {
return (it != cacheCoins.end() && !it->second.coin.IsSpent());
}
+std::optional CCoinsViewCache::GetPossiblySpentCoinFromCache(const COutPoint& outpoint) const noexcept
+{
+ if (auto it{cacheCoins.find(outpoint)}; it != cacheCoins.end()) return it->second.coin;
+ return std::nullopt;
+}
+
uint256 CCoinsViewCache::GetBestBlock() const {
if (hashBlock.IsNull())
hashBlock = base->GetBestBlock();
@@ -185,18 +191,16 @@ void CCoinsViewCache::SetBestBlock(const uint256 &hashBlockIn) {
bool CCoinsViewCache::BatchWrite(CoinsViewCacheCursor& cursor, const uint256 &hashBlockIn) {
for (auto it{cursor.Begin()}; it != cursor.End(); it = cursor.NextAndMaybeErase(*it)) {
- // Ignore non-dirty entries (optimization).
- if (!it->second.IsDirty()) {
+ if (!it->second.IsDirty()) { // TODO a cursor can only contain dirty entries
continue;
}
- CCoinsMap::iterator itUs = cacheCoins.find(it->first);
- if (itUs == cacheCoins.end()) {
- // The parent cache does not have an entry, while the child cache does.
- // We can ignore it if it's both spent and FRESH in the child
- if (!(it->second.IsFresh() && it->second.coin.IsSpent())) {
- // Create the coin in the parent cache, move the data up
- // and mark it as dirty.
- itUs = cacheCoins.try_emplace(it->first).first;
+ auto [itUs, inserted]{cacheCoins.try_emplace(it->first)};
+ if (inserted) {
+ if (it->second.IsFresh() && it->second.coin.IsSpent()) {
+ cacheCoins.erase(itUs); // TODO fresh coins should have been removed at spend
+ } else {
+ // The parent cache does not have an entry, while the child cache does.
+ // Move the data up and mark it as dirty.
CCoinsCacheEntry& entry{itUs->second};
assert(entry.coin.DynamicMemoryUsage() == 0);
if (cursor.WillErase(*it)) {
diff --git a/src/coins.h b/src/coins.h
index 2fcc764a3fdf..9ae602165dcc 100644
--- a/src/coins.h
+++ b/src/coins.h
@@ -401,6 +401,14 @@ class CCoinsViewCache : public CCoinsViewBacked
*/
bool HaveCoinInCache(const COutPoint &outpoint) const;
+ /**
+ * Retrieve the coin from the cache even if it is spent, without calling
+ * the backing CCoinsView if no coin exists.
+ * Used in CoinsViewCacheAsync to make sure we do not add a coin from the backing
+ * view when it is spent in the cache but not yet flushed to the parent.
+ */
+ std::optional GetPossiblySpentCoinFromCache(const COutPoint& outpoint) const noexcept;
+
/**
* Return a reference to Coin in the cache, or coinEmpty if not found. This is
* more efficient than GetCoin.
@@ -441,7 +449,7 @@ class CCoinsViewCache : public CCoinsViewBacked
* to be forgotten.
* If false is returned, the state of this cache (and its backing view) will be undefined.
*/
- bool Flush();
+ virtual bool Flush();
/**
* Push the modifications applied to this cache to its base while retaining
@@ -482,7 +490,7 @@ class CCoinsViewCache : public CCoinsViewBacked
* @note this is marked const, but may actually append to `cacheCoins`, increasing
* memory usage.
*/
- CCoinsMap::iterator FetchCoin(const COutPoint &outpoint) const;
+ virtual CCoinsMap::iterator FetchCoin(const COutPoint &outpoint) const;
};
//! Utility function to add all of a transaction's outputs to a cache.
diff --git a/src/coinsviewcacheasync.h b/src/coinsviewcacheasync.h
new file mode 100644
index 000000000000..7114258060e2
--- /dev/null
+++ b/src/coinsviewcacheasync.h
@@ -0,0 +1,247 @@
+// Copyright (c) The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+#ifndef BITCOIN_COINSVIEWCACHEASYNC_H
+#define BITCOIN_COINSVIEWCACHEASYNC_H
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+static constexpr int32_t WORKER_THREADS{4};
+
+/**
+ * CCoinsViewCache subclass that asynchronously fetches block inputs in parallel.
+ * Only used in ConnectBlock to pass as an ephemeral view that can be reset if the block is invalid.
+ * It provides the same interface as CCoinsViewCache, overriding the FetchCoin private method and Flush.
+ * It adds an additional StartFetching method to provide the block, and Reset to reset the state if Flush is not called.
+ *
+ * The cache spawns a fixed set of worker threads that fetch Coins for each input in a block.
+ * When FetchCoin() is called, the main thread waits for the corresponding coin to be fetched and returns it.
+ * While waiting, the main thread will also fetch coins to maximize parallelism.
+ *
+ * Worker threads are synchronized with the main thread using a barrier, which is used at the beginning of fetching to
+ * start the workers and at the end to ensure all workers have finished before the next block is started.
+ */
+class CoinsViewCacheAsync : public CCoinsViewCache
+{
+private:
+ //! The latest input not yet being fetched. Workers atomically increment this when fetching.
+ mutable std::atomic_uint32_t m_input_head{0};
+ //! The latest input not yet accessed by a consumer. Only the main thread increments this.
+ mutable uint32_t m_input_tail{0};
+
+ //! The inputs of the block which is being fetched.
+ struct InputToFetch {
+ //! Workers set this after setting the coin. The main thread tests this before reading the coin.
+ std::atomic_flag ready{};
+ //! The outpoint of the input to fetch;
+ const COutPoint& outpoint;
+ //! The coin that workers will fetch and main thread will insert into cache.
+ std::optional coin{std::nullopt};
+
+ /**
+ * We only move when m_inputs reallocates during setup.
+ * We never move after work begins, so we don't have to copy other members.
+ */
+ InputToFetch(InputToFetch&& other) noexcept : outpoint{other.outpoint} {}
+ explicit InputToFetch(const COutPoint& o LIFETIMEBOUND) noexcept : outpoint{o} {}
+ };
+ mutable std::vector m_inputs{};
+
+ /**
+ * The first 8 bytes of txids of all txs in the block being fetched. This is used to filter out inputs that
+ * are created earlier in the same block, since they will not be in the db or the cache.
+ * Using only the first 8 bytes is a performance improvement, versus storing the entire 32 bytes. In case of a
+ * collision of an input being spent having the same first 8 bytes as a txid of a tx elsewhere in the block,
+ * the input will not be fetched in the background. The input will still be fetched later on the main thread.
+ * Using a sorted vector and binary search lookups is a performance improvement. It is faster than
+ * using std::unordered_set with salted hash or std::set.
+ */
+ std::vector m_txids{};
+
+ //! DB coins view to fetch from.
+ const CCoinsView& m_db;
+
+ /**
+ * Similar to CCoinsViewCache::GetCoin, but it does not mutate internally.
+ * Therefore safe to call from any thread once inside the barrier.
+ */
+ std::optional GetCoinWithoutMutating(const COutPoint& outpoint) const
+ {
+ if (auto coin{static_cast(base)->GetPossiblySpentCoinFromCache(outpoint)}) {
+ if (!coin->IsSpent()) [[likely]] return coin;
+ return std::nullopt;
+ }
+ return m_db.GetCoin(outpoint);
+ }
+
+ /**
+ * Claim and fetch the next input in the queue. Safe to call from any thread once inside the barrier.
+ *
+ * @return true if there are more inputs in the queue to fetch
+ * @return false if there are no more inputs in the queue to fetch
+ */
+ bool ProcessInputInBackground() const noexcept
+ {
+ const auto i{m_input_head.fetch_add(1, std::memory_order_relaxed)};
+ if (i >= m_inputs.size()) [[unlikely]] return false;
+
+ auto& input{m_inputs[i]};
+ // Inputs spending a coin from a tx earlier in the block won't be in the cache or db
+ if (std::ranges::binary_search(m_txids, input.outpoint.hash.ToUint256().GetUint64(0))) {
+ // We can use relaxed ordering here since we don't write the coin.
+ input.ready.test_and_set(std::memory_order_relaxed);
+ input.ready.notify_one();
+ return true;
+ }
+
+ if (auto coin{GetCoinWithoutMutating(input.outpoint)}) [[likely]] input.coin.emplace(std::move(*coin));
+ // We need release here, so writing coin in the line above happens before the main thread acquires.
+ input.ready.test_and_set(std::memory_order_release);
+ input.ready.notify_one();
+ return true;
+ }
+
+ //! Get the index in m_inputs for the given outpoint. Advances m_input_tail if found.
+ std::optional GetInputIndex(const COutPoint& outpoint) const noexcept
+ {
+ // This assumes ConnectBlock accesses all inputs in the same order as they are added to m_inputs
+ // in StartFetching. Some outpoints are not accessed because they are created by the block, so we scan until we
+ // come across the requested input. The input will be cached after access, so we can advance the tail so
+ // future accesses won't have to scan previously accessed inputs.
+ for (const auto i : std::views::iota(m_input_tail, m_inputs.size())) [[likely]] {
+ if (m_inputs[i].outpoint == outpoint) {
+ m_input_tail = i + 1;
+ return i;
+ }
+ }
+ return std::nullopt;
+ }
+
+ CCoinsMap::iterator FetchCoin(const COutPoint& outpoint) const override
+ {
+ const auto& [ret, inserted]{cacheCoins.try_emplace(outpoint)};
+ if (!inserted) return ret;
+
+ if (const auto i{GetInputIndex(outpoint)}) [[likely]] {
+ auto& input{m_inputs[*i]};
+ // Check if the coin is ready to be read. We need to acquire to match the worker thread's release.
+ while (!input.ready.test(std::memory_order_acquire)) {
+ // Work instead of waiting if the coin is not ready
+ if (!ProcessInputInBackground()) {
+ // No more work, just wait
+ input.ready.wait(/*old=*/false, std::memory_order_acquire);
+ break;
+ }
+ }
+ if (input.coin) [[likely]] ret->second.coin = std::move(*input.coin);
+ }
+
+ if (ret->second.coin.IsSpent()) [[unlikely]] {
+ // We will only get in here for BIP30 checks, txid collisions, or a block with missing or spent inputs.
+ if (auto coin{GetCoinWithoutMutating(outpoint)}) {
+ ret->second.coin = std::move(*coin);
+ } else {
+ cacheCoins.erase(ret);
+ return cacheCoins.end();
+ }
+ }
+
+ cachedCoinsUsage += ret->second.coin.DynamicMemoryUsage();
+ return ret;
+ }
+
+ std::vector m_worker_threads{};
+ std::barrier<> m_barrier;
+
+ //! Stop all worker threads.
+ void StopFetching() noexcept
+ {
+ if (m_inputs.empty()) return;
+ // Skip fetching the rest of the inputs by moving the head to the end.
+ m_input_head.store(m_inputs.size(), std::memory_order_relaxed);
+ // Wait for all threads to stop.
+ m_barrier.arrive_and_wait();
+ m_inputs.clear();
+ }
+
+public:
+ //! Start fetching all block inputs in parallel.
+ void StartFetching(const CBlock& block) noexcept
+ {
+ // Loop through the inputs of the block and set them in the queue. Also construct the set of txids to filter.
+ for (const auto& tx : block.vtx | std::views::drop(1)) [[likely]] {
+ for (const auto& input : tx->vin) [[likely]] m_inputs.emplace_back(input.prevout);
+ m_txids.emplace_back(tx->GetHash().ToUint256().GetUint64(0));
+ }
+ // Don't start threads if there's nothing to fetch.
+ if (m_inputs.empty()) [[unlikely]] return;
+ // Sort txids so we can do binary search lookups.
+ std::ranges::sort(m_txids);
+ // Start workers by entering the barrier.
+ m_barrier.arrive_and_wait();
+ }
+
+ //! Stop fetching and reset state. Must be called before block is destroyed.
+ void Reset() noexcept
+ {
+ StopFetching();
+ m_input_head.store(0, std::memory_order_relaxed);
+ m_input_tail = 0;
+ m_txids.clear();
+ cacheCoins.clear();
+ cachedCoinsUsage = 0;
+ hashBlock = uint256::ZERO;
+ }
+
+ bool Flush() override
+ {
+ // We need to stop workers from accessing base before we mutate it.
+ StopFetching();
+ auto cursor{CoinsViewCacheCursor(m_sentinel, cacheCoins, /*will_erase=*/true)};
+ const auto ret{base->BatchWrite(cursor, hashBlock)};
+ Reset();
+ return ret;
+ }
+
+ explicit CoinsViewCacheAsync(CCoinsViewCache& cache, const CCoinsView& db,
+ int32_t num_workers = WORKER_THREADS) noexcept
+ : CCoinsViewCache{&cache}, m_db{db}, m_barrier{num_workers + 1}
+ {
+ for (const auto n : std::views::iota(0, num_workers)) {
+ m_worker_threads.emplace_back([this, n] {
+ util::ThreadRename(strprintf("inputfetch.%i", n));
+ while (true) {
+ m_barrier.arrive_and_wait();
+ while (ProcessInputInBackground()) [[likely]] {}
+ if (m_inputs.empty()) [[unlikely]] return;
+ m_barrier.arrive_and_wait();
+ }
+ });
+ }
+ }
+
+ ~CoinsViewCacheAsync() override
+ {
+ m_barrier.arrive_and_drop();
+ for (auto& t : m_worker_threads) t.join();
+ }
+};
+
+#endif // BITCOIN_COINSVIEWCACHEASYNC_H
diff --git a/src/test/CMakeLists.txt b/src/test/CMakeLists.txt
index 83cb989aa9b1..96c95cc4571c 100644
--- a/src/test/CMakeLists.txt
+++ b/src/test/CMakeLists.txt
@@ -32,6 +32,7 @@ add_executable(test_bitcoin
cluster_linearize_tests.cpp
coins_tests.cpp
coinscachepair_tests.cpp
+ coinsviewcacheasync_tests.cpp
coinstatsindex_tests.cpp
common_url_tests.cpp
compress_tests.cpp
diff --git a/src/test/coinsviewcacheasync_tests.cpp b/src/test/coinsviewcacheasync_tests.cpp
new file mode 100644
index 000000000000..54425647a085
--- /dev/null
+++ b/src/test/coinsviewcacheasync_tests.cpp
@@ -0,0 +1,221 @@
+// Copyright (c) The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+
+#include
+#include
+#include
+#include
+
+BOOST_AUTO_TEST_SUITE(coinsviewcacheasync_tests)
+
+struct NoAccessCoinsView : CCoinsView {
+ std::optional GetCoin(const COutPoint&) const override { abort(); }
+};
+
+static CBlock CreateBlock() noexcept
+{
+ static constexpr auto NUM_TXS{100};
+ CBlock block;
+ CMutableTransaction coinbase;
+ coinbase.vin.emplace_back();
+ block.vtx.push_back(MakeTransactionRef(coinbase));
+
+ Txid prevhash{Txid::FromUint256(uint256{1})};
+
+ for (const auto i : std::views::iota(1, NUM_TXS)) {
+ CMutableTransaction tx;
+ Txid txid;
+ if (i % 3 == 0) {
+ // External input
+ txid = Txid::FromUint256(uint256(i));
+ } else if (i % 3 == 1) {
+ // Internal spend (prev tx)
+ txid = prevhash;
+ } else {
+ // Test shortid collisions (looks internal, but is external)
+ uint256 u{};
+ std::memcpy(u.begin(), prevhash.ToUint256().begin(), 8);
+ txid = Txid::FromUint256(u);
+ }
+ tx.vin.emplace_back(txid, 0);
+ prevhash = tx.GetHash();
+ block.vtx.push_back(MakeTransactionRef(tx));
+ }
+
+ return block;
+}
+
+void PopulateView(const CBlock& block, CCoinsView& view, bool spent = false)
+{
+ CCoinsViewCache cache{&view};
+ cache.SetBestBlock(uint256::ONE);
+
+ std::unordered_set txids{};
+ txids.reserve(block.vtx.size() - 1);
+ for (const auto& tx : block.vtx | std::views::drop(1)) {
+ for (const auto& in : tx->vin) {
+ if (!txids.contains(in.prevout.hash)) {
+ Coin coin{};
+ if (!spent) coin.out.nValue = 1;
+ cache.EmplaceCoinInternalDANGER(COutPoint{in.prevout}, std::move(coin));
+ }
+ }
+ txids.emplace(tx->GetHash());
+ }
+
+ cache.Flush();
+}
+
+void CheckCache(const CBlock& block, const CCoinsViewCache& cache)
+{
+ uint32_t counter{0};
+ std::unordered_set txids{};
+ txids.reserve(block.vtx.size() - 1);
+
+ for (const auto& tx : block.vtx) {
+ if (tx->IsCoinBase()) {
+ BOOST_CHECK(!cache.GetPossiblySpentCoinFromCache(tx->vin[0].prevout));
+ } else {
+ for (const auto& in : tx->vin) {
+ const auto& outpoint{in.prevout};
+ const auto& first{cache.AccessCoin(outpoint)};
+ const auto& second{cache.AccessCoin(outpoint)};
+ BOOST_CHECK_EQUAL(&first, &second);
+ const auto should_have{!txids.contains(outpoint.hash)};
+ if (should_have) ++counter;
+ const auto have{cache.GetPossiblySpentCoinFromCache(outpoint)};
+ BOOST_CHECK_EQUAL(should_have, !!have);
+ }
+ txids.emplace(tx->GetHash());
+ }
+ }
+ BOOST_CHECK_EQUAL(cache.GetCacheSize(), counter);
+}
+
+
+BOOST_AUTO_TEST_CASE(fetch_inputs_from_db)
+{
+ const auto block{CreateBlock()};
+ CCoinsViewDB db{{.path = "", .cache_bytes = 1_MiB, .memory_only = true}, {}};
+ PopulateView(block, db);
+ NoAccessCoinsView no_access;
+ CCoinsViewCache main_cache{&no_access};
+ CoinsViewCacheAsync view{main_cache, db};
+ for (auto i{0}; i < 3; ++i) {
+ view.StartFetching(block);
+ CheckCache(block, view);
+ view.Reset();
+ }
+}
+
+BOOST_AUTO_TEST_CASE(fetch_inputs_from_cache)
+{
+ const auto block{CreateBlock()};
+ const CCoinsViewDB db{{.path = "", .cache_bytes = 1_MiB, .memory_only = true}, {}};
+ NoAccessCoinsView no_access;
+ CCoinsViewCache main_cache{&no_access};
+ PopulateView(block, main_cache);
+ CoinsViewCacheAsync view{main_cache, db};
+ for (auto i{0}; i < 3; ++i) {
+ view.StartFetching(block);
+ CheckCache(block, view);
+ view.Reset();
+ }
+}
+
+// Test for the case where a block spends coins that are spent in the cache, but
+// the spentness has not been flushed to the db.
+BOOST_AUTO_TEST_CASE(fetch_no_double_spend)
+{
+ const auto block{CreateBlock()};
+ CCoinsViewDB db{{.path = "", .cache_bytes = 1_MiB, .memory_only = true}, {}};
+ PopulateView(block, db);
+ NoAccessCoinsView no_access;
+ CCoinsViewCache main_cache{&no_access};
+ // Add all inputs as spent already in cache
+ PopulateView(block, main_cache, /*spent=*/true);
+ CoinsViewCacheAsync view{main_cache, db};
+ for (auto i{0}; i < 3; ++i) {
+ view.StartFetching(block);
+ for (const auto& tx : block.vtx) {
+ for (const auto& in : tx->vin) {
+ const auto& c{view.AccessCoin(in.prevout)};
+ BOOST_CHECK(c.IsSpent());
+ }
+ }
+ // Coins are not added to the view, even though they exist unspent in the parent db
+ BOOST_CHECK_EQUAL(view.GetCacheSize(), 0);
+ view.Reset();
+ }
+}
+
+BOOST_AUTO_TEST_CASE(fetch_no_inputs)
+{
+ const auto block{CreateBlock()};
+ const CCoinsViewDB db{{.path = "", .cache_bytes = 1_MiB, .memory_only = true}, {}};
+ NoAccessCoinsView no_access;
+ CCoinsViewCache main_cache{&no_access};
+ CoinsViewCacheAsync view{main_cache, db};
+ for (auto i{0}; i < 3; ++i) {
+ view.StartFetching(block);
+ for (const auto& tx : block.vtx) {
+ for (const auto& in : tx->vin) {
+ const auto& c{view.AccessCoin(in.prevout)};
+ BOOST_CHECK(c.IsSpent());
+ }
+ }
+ BOOST_CHECK_EQUAL(view.GetCacheSize(), 0);
+ view.Reset();
+ }
+}
+
+// Test that the main thread can make progress with no workers
+BOOST_AUTO_TEST_CASE(fetch_main_thread)
+{
+ const auto block{CreateBlock()};
+ const CCoinsViewDB db{{.path = "", .cache_bytes = 1_MiB, .memory_only = true}, {}};
+ NoAccessCoinsView no_access;
+ CCoinsViewCache main_cache{&no_access};
+ PopulateView(block, main_cache);
+ CoinsViewCacheAsync view{main_cache, db, /*num_workers=*/0};
+ for (auto i{0}; i < 3; ++i) {
+ view.StartFetching(block);
+ CheckCache(block, view);
+ view.Reset();
+ }
+}
+
+// Access coin that is not a block's input
+BOOST_AUTO_TEST_CASE(access_non_input_coin)
+{
+ const auto block{CreateBlock()};
+ const CCoinsViewDB db{{.path = "", .cache_bytes = 1_MiB, .memory_only = true}, {}};
+ NoAccessCoinsView no_access;
+ CCoinsViewCache main_cache{&no_access};
+ Coin coin{};
+ coin.out.nValue = 1;
+ const COutPoint outpoint{Txid::FromUint256(uint256::ZERO), 0};
+ main_cache.EmplaceCoinInternalDANGER(COutPoint{Txid::FromUint256(uint256::ZERO), 0}, std::move(coin));
+ CoinsViewCacheAsync view{main_cache, db, /*num_workers=*/0};
+ for (auto i{0}; i < 3; ++i) {
+ view.StartFetching(block);
+ const auto& accessed_coin{view.AccessCoin(outpoint)};
+ BOOST_CHECK(!accessed_coin.IsSpent());
+ view.Reset();
+ }
+}
+
+BOOST_AUTO_TEST_SUITE_END()
diff --git a/src/test/fuzz/CMakeLists.txt b/src/test/fuzz/CMakeLists.txt
index 607723b978ae..2bbda8e88efc 100644
--- a/src/test/fuzz/CMakeLists.txt
+++ b/src/test/fuzz/CMakeLists.txt
@@ -27,6 +27,7 @@ add_executable(fuzz
cluster_linearize.cpp
coins_view.cpp
coinscache_sim.cpp
+ coinsviewcacheasync.cpp
connman.cpp
crypto.cpp
crypto_aes256.cpp
diff --git a/src/test/fuzz/coinsviewcacheasync.cpp b/src/test/fuzz/coinsviewcacheasync.cpp
new file mode 100644
index 000000000000..187d0e49d7a5
--- /dev/null
+++ b/src/test/fuzz/coinsviewcacheasync.cpp
@@ -0,0 +1,181 @@
+// Copyright (c) The Bitcoin Core developers
+// Distributed under the MIT software license, see the accompanying
+// file COPYING or http://www.opensource.org/licenses/mit-license.php.
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+std::optional g_async_cache{};
+std::optional g_db{};
+
+static void setup_threadpool_test()
+{
+ LogInstance().DisableLogging();
+ auto db_params = DBParams{
+ .path = "",
+ .cache_bytes = 1_MiB,
+ .memory_only = true,
+ };
+ g_db.emplace(std::move(db_params), CoinsViewOptions{});
+ CCoinsViewCache cache{nullptr};
+ g_async_cache.emplace(cache, *g_db);
+}
+
+FUZZ_TARGET(coinsviewcacheasync, .init = setup_threadpool_test)
+{
+ SeedRandomStateForTest(SeedRand::ZEROS);
+ FuzzedDataProvider fuzzed_data_provider(buffer.data(), buffer.size());
+
+ LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 10000)
+ {
+ CBlock block;
+ Txid prevhash{Txid::FromUint256(ConsumeUInt256(fuzzed_data_provider))};
+
+ std::map db_map{};
+ std::map cache_map{};
+ std::vector input_outpoints{};
+
+ CCoinsViewCache main_cache(&*g_db);
+ // Used for writing to the db and erasing between iterations
+ CCoinsViewCache dummy_cache(&*g_db);
+ dummy_cache.SetBestBlock(uint256::ONE);
+
+ LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 10000)
+ {
+ CMutableTransaction tx;
+
+ LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), 10000)
+ {
+ Txid txid;
+ if (fuzzed_data_provider.ConsumeBool()) {
+ txid = Txid::FromUint256(ConsumeUInt256(fuzzed_data_provider));
+ } else if (fuzzed_data_provider.ConsumeBool()) {
+ txid = prevhash;
+ } else {
+ // Test shortid collisions
+ uint256 u{ConsumeUInt256(fuzzed_data_provider)};
+ std::memcpy(u.begin(), prevhash.ToUint256().begin(), 8);
+ txid = Txid::FromUint256(u);
+ }
+ const auto index{fuzzed_data_provider.ConsumeIntegral()};
+ const COutPoint outpoint{txid, index};
+
+ tx.vin.emplace_back(outpoint);
+
+ if (fuzzed_data_provider.ConsumeBool()) {
+ Coin coin{};
+ coin.fCoinBase = fuzzed_data_provider.ConsumeBool();
+ coin.nHeight =
+ fuzzed_data_provider.ConsumeIntegralInRange(
+ 0, std::numeric_limits::max());
+ coin.out.nValue = ConsumeMoney(fuzzed_data_provider);
+ assert(!coin.IsSpent());
+ db_map.try_emplace(outpoint, coin);
+ dummy_cache.EmplaceCoinInternalDANGER(
+ COutPoint(outpoint),
+ std::move(coin));
+ }
+
+ // Add a different coin to the cache
+ if (fuzzed_data_provider.ConsumeBool()) {
+ Coin coin{};
+ coin.fCoinBase = fuzzed_data_provider.ConsumeBool();
+ coin.nHeight =
+ fuzzed_data_provider.ConsumeIntegralInRange(
+ 0, std::numeric_limits::max());
+ coin.out.nValue =
+ fuzzed_data_provider.ConsumeIntegralInRange(
+ -1, MAX_MONEY);
+ cache_map.try_emplace(outpoint, coin);
+ main_cache.EmplaceCoinInternalDANGER(
+ COutPoint(outpoint),
+ std::move(coin));
+ }
+
+ input_outpoints.emplace_back(outpoint);
+ }
+
+ prevhash = tx.GetHash();
+ block.vtx.push_back(MakeTransactionRef(tx));
+ }
+
+ (void)dummy_cache.Sync();
+ CoinsViewCacheAsync& cache(*g_async_cache);
+ cache.SetBackend(main_cache);
+ cache.StartFetching(block);
+
+ std::unordered_set outpoints_in_cache{};
+ LIMITED_WHILE(fuzzed_data_provider.ConsumeBool(), static_cast(input_outpoints.size() * 10))
+ {
+ COutPoint outpoint;
+ if (fuzzed_data_provider.ConsumeBool()) {
+ const auto index{fuzzed_data_provider.ConsumeIntegralInRange(0, input_outpoints.size() - 1)};
+ outpoint = input_outpoints[index];
+ } else {
+ const auto txid{Txid::FromUint256(ConsumeUInt256(fuzzed_data_provider))};
+ const auto index{fuzzed_data_provider.ConsumeIntegral()};
+ outpoint = COutPoint(txid, index);
+ }
+ const auto& accessed_coin{cache.AccessCoin(outpoint)};
+ const auto coin{cache.GetPossiblySpentCoinFromCache(outpoint)};
+ if (coin) {
+ assert(!coin->IsSpent());
+ assert(coin->fCoinBase == accessed_coin.fCoinBase);
+ assert(coin->nHeight == accessed_coin.nHeight);
+ assert(coin->out == accessed_coin.out);
+ outpoints_in_cache.emplace(outpoint);
+ }
+ const auto& db_it{db_map.find(outpoint)};
+ const auto cache_it{cache_map.find(outpoint)};
+ if (!coin) {
+ assert(accessed_coin.IsSpent());
+ // If we don't have a coin, then it's either spent in cache or missing
+ const auto spent_cache_coin{cache_it != cache_map.end() && cache_it->second.IsSpent()};
+ const auto no_coin{cache_it == cache_map.end() && db_it == db_map.end()};
+ assert(spent_cache_coin || no_coin);
+ } else if (cache_it != cache_map.end()) {
+ // Make sure we have the main cache coin if it exists instead of db
+ const auto& cache_coin{cache_it->second};
+ assert(!cache_coin.IsSpent());
+ assert(coin->fCoinBase == cache_coin.fCoinBase);
+ assert(coin->nHeight == cache_coin.nHeight);
+ assert(coin->out == cache_coin.out);
+ } else {
+ assert(db_it != db_map.end());
+ // Check any coins not in the main cache are the same as the db
+ const auto& db_coin{db_it->second};
+ assert(coin->fCoinBase == db_coin.fCoinBase);
+ assert(coin->nHeight == db_coin.nHeight);
+ assert(coin->out == db_coin.out);
+ }
+ }
+ assert(cache.GetCacheSize() == outpoints_in_cache.size());
+ fuzzed_data_provider.ConsumeBool() ? (void)cache.Flush() : cache.Reset();
+ for (const auto& pair : db_map) {
+ dummy_cache.SpendCoin(pair.first);
+ }
+ (void)dummy_cache.Flush();
+ }
+}
diff --git a/src/validation.cpp b/src/validation.cpp
index 507329655817..490950730dc6 100644
--- a/src/validation.cpp
+++ b/src/validation.cpp
@@ -1872,6 +1872,7 @@ void CoinsViews::InitCache()
{
AssertLockHeld(::cs_main);
m_cacheview = std::make_unique(&m_catcherview);
+ m_connect_block_view = std::make_unique(*m_cacheview, m_catcherview);
}
Chainstate::Chainstate(
@@ -3083,7 +3084,8 @@ bool Chainstate::ConnectTip(
LogDebug(BCLog::BENCH, " - Load block from disk: %.2fms\n",
Ticks(time_2 - time_1));
{
- CCoinsViewCache view(&CoinsTip());
+ auto& view{*m_coins_views->m_connect_block_view};
+ view.StartFetching(*block_to_connect);
bool rv = ConnectBlock(*block_to_connect, state, pindexNew, view);
if (m_chainman.m_options.signals) {
m_chainman.m_options.signals->BlockChecked(block_to_connect, state);
@@ -3092,6 +3094,7 @@ bool Chainstate::ConnectTip(
if (state.IsInvalid())
InvalidBlockFound(pindexNew, state);
LogError("%s: ConnectBlock %s failed, %s\n", __func__, pindexNew->GetBlockHash().ToString(), state.ToString());
+ view.Reset();
return false;
}
time_3 = SteadyClock::now();
diff --git a/src/validation.h b/src/validation.h
index cd448f3ca9eb..efb0a8b52d25 100644
--- a/src/validation.h
+++ b/src/validation.h
@@ -10,6 +10,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -485,6 +486,10 @@ class CoinsViews {
//! can fit per the dbcache setting.
std::unique_ptr m_cacheview GUARDED_BY(cs_main);
+ //! Used as an empty view that is only passed into ConnectBlock to help speed up block validation,
+ //! as well as not pollute the underlying cache with newly created coins in case the block is invalid.
+ std::unique_ptr m_connect_block_view GUARDED_BY(cs_main);
+
//! This constructor initializes CCoinsViewDB and CCoinsViewErrorCatcher instances, but it
//! *does not* create a CCoinsViewCache instance by default. This is done separately because the
//! presence of the cache has implications on whether or not we're allowed to flush the cache's