view rust/hg-core/src/revlog/nodemap_docket.rs @ 48178:f12a19d03d2c

fix: reduce number of tool executions By grouping together (path, ctx) pairs according to the inputs they would provide to fixer tools, we can deduplicate executions of fixer tools to significantly reduce the amount of time spent running slow tools. This change does not handle clean files in the working copy, which could still be deduplicated against the files in the checked out commit. It's a little harder to do that because the filerev is not available in the workingfilectx (and it doesn't exist for added files). Anecdotally, this change makes some real uses cases at Google 10x faster. I think we were originally hesitant to do this because the benefits weren't obvious, and implementing it efficiently is kind of tricky. If we simply memoized the formatter execution function, we would be keeping tons of file content in memory. Also included is a regression test for a corner case that I broke with my first attempt at optimizing this code. Differential Revision: https://phab.mercurial-scm.org/D11280
author Danny Hooper <hooper@google.com>
date Thu, 02 Sep 2021 14:08:45 -0700
parents 001d747c2baf
children 07d8d144c222
line wrap: on
line source

use crate::errors::{HgError, HgResultExt};
use crate::requirements;
use bytes_cast::{unaligned, BytesCast};
use memmap2::Mmap;
use std::path::{Path, PathBuf};

use crate::repo::Repo;
use crate::utils::strip_suffix;

const ONDISK_VERSION: u8 = 1;

pub(super) struct NodeMapDocket {
    pub data_length: usize,
    // TODO: keep here more of the data from `parse()` when we need it
}

#[derive(BytesCast)]
#[repr(C)]
struct DocketHeader {
    uid_size: u8,
    _tip_rev: unaligned::U64Be,
    data_length: unaligned::U64Be,
    _data_unused: unaligned::U64Be,
    tip_node_size: unaligned::U64Be,
}

impl NodeMapDocket {
    /// Return `Ok(None)` when the caller should proceed without a persistent
    /// nodemap:
    ///
    /// * This revlog does not have a `.n` docket file (it is not generated for
    ///   small revlogs), or
    /// * The docket has an unsupported version number (repositories created by
    ///   later hg, maybe that should be a requirement instead?), or
    /// * The docket file points to a missing (likely deleted) data file (this
    ///   can happen in a rare race condition).
    pub fn read_from_file(
        repo: &Repo,
        index_path: &Path,
    ) -> Result<Option<(Self, Mmap)>, HgError> {
        if !repo
            .requirements()
            .contains(requirements::NODEMAP_REQUIREMENT)
        {
            // If .hg/requires does not opt it, don’t try to open a nodemap
            return Ok(None);
        }

        let docket_path = index_path.with_extension("n");
        let docket_bytes = if let Some(bytes) =
            repo.store_vfs().read(&docket_path).io_not_found_as_none()?
        {
            bytes
        } else {
            return Ok(None);
        };

        let input = if let Some((&ONDISK_VERSION, rest)) =
            docket_bytes.split_first()
        {
            rest
        } else {
            return Ok(None);
        };

        /// Treat any error as a parse error
        fn parse<T, E>(result: Result<T, E>) -> Result<T, HgError> {
            result
                .map_err(|_| HgError::corrupted("nodemap docket parse error"))
        }

        let (header, rest) = parse(DocketHeader::from_bytes(input))?;
        let uid_size = header.uid_size as usize;
        // TODO: do we care about overflow for 4 GB+ nodemap files on 32-bit
        // systems?
        let tip_node_size = header.tip_node_size.get() as usize;
        let data_length = header.data_length.get() as usize;
        let (uid, rest) = parse(u8::slice_from_bytes(rest, uid_size))?;
        let (_tip_node, _rest) =
            parse(u8::slice_from_bytes(rest, tip_node_size))?;
        let uid = parse(std::str::from_utf8(uid))?;
        let docket = NodeMapDocket { data_length };

        let data_path = rawdata_path(&docket_path, uid);
        // TODO: use `vfs.read()` here when the `persistent-nodemap.mmap`
        // config is false?
        if let Some(mmap) = repo
            .store_vfs()
            .mmap_open(&data_path)
            .io_not_found_as_none()?
        {
            if mmap.len() >= data_length {
                Ok(Some((docket, mmap)))
            } else {
                Err(HgError::corrupted("persistent nodemap too short"))
            }
        } else {
            // Even if .hg/requires opted in, some revlogs are deemed small
            // enough to not need a persistent nodemap.
            Ok(None)
        }
    }
}

fn rawdata_path(docket_path: &Path, uid: &str) -> PathBuf {
    let docket_name = docket_path
        .file_name()
        .expect("expected a base name")
        .to_str()
        .expect("expected an ASCII file name in the store");
    let prefix = strip_suffix(docket_name, ".n.a")
        .or_else(|| strip_suffix(docket_name, ".n"))
        .expect("expected docket path in .n or .n.a");
    let name = format!("{}-{}.nd", prefix, uid);
    docket_path
        .parent()
        .expect("expected a non-root path")
        .join(name)
}