Viewing files at commit f08b4a249972Commit detailsView latest

suites/scale/scalesuite/tiers.py 215 lines · 11.0 KB · python
1
"""Repository tiers and the 14-shape core matrix as runnable fixture recipes.
2
3
Tiers re-implement the *concept* of git's t/perf repository tiers (the
4
``test_perf_fresh_repo`` / ``test_perf_default_repo`` / ``test_perf_large_repo``
5
helpers in t/perf/perf-lib.sh, GPL-2.0: concept only, no text or code) with
6
deterministic synthetic repositories instead of copies of git.git/linux:
7
8
- ``fresh``: fixed-overhead tier; a just-created repository with one commit;
9
- ``default``: an application-sized repository (git.git-ish order of files,
10
  modest history, some refs);
11
- ``large``: monorepo pressure (100k files, 10k commits, 1k branches, a
12
  256 MiB binary with revisions);
13
- ``xl-*``: one axis pushed past real-world "large" per fixture, opt-in with
14
  ``--allow-xl`` (history 100k commits, tree 1M files, deep 40-level paths,
15
  refs 10k branches + 2k tags, a multi-GiB binary). ``xl-combined`` puts every
16
  axis in one repository and additionally needs ``--allow-xl-combined``.
17
18
Shapes: every scenario of scenarios/core.yaml (the 14-shape catalog that no
19
lane ran) maps to a GenSpec via its ``fixture_shape`` (text/binary counts and
20
sizes, path depth and fan-out, history, branches, ignore patterns, conflict
21
shape, dirty shape). ``scale`` shrinks counts (never depth) for development
22
runs; the scale factor is part of the fixture id and spec hash.
23
"""
24
25
from __future__ import annotations
26
27
import math
28
from dataclasses import dataclass, field, replace
29
from pathlib import Path
30
from typing import Any, Dict, List, Optional, Tuple
31
32
from .generator import GenSpec
33
34
ROOT = Path(__file__).resolve().parents[3]
35
CORE_YAML = ROOT / "scenarios" / "core.yaml"
36
GIB = 1 << 30
37
MIB = 1 << 20
38
39
40
@dataclass(frozen=True)
41
class DirtyShape:
42
    modified: int
43
    added: int
44
    deleted: int
45
    renamed: int
46
47
48
@dataclass(frozen=True)
49
class Tier:
50
    tier_id: str
51
    kind: str  # "tier" | "xl" | "shape"
52
    spec: GenSpec
53
    dirty: DirtyShape
54
    ignored_extras: int = 0
55
    xl: bool = False
56
    combined: bool = False
57
    # Operations that are too costly per run at this size (or meaningless for
58
    # the shape) are listed here and emitted as explicit skip rows.
59
    skip_ops: Tuple[str, ...] = ()
60
    runs_hint: int = 5
61
    # False: no sync clients/servers (disk budget, e.g. multi-GiB blobs x 4
62
    # checkouts); push/pull/clone become skip rows naming the reason.
63
    sync: bool = True
64
    notes: str = ""
65
66
    @property
67
    def fixture_id(self) -> str:
68
        return f"scale-{self.tier_id}-{self.spec.spec_sha256()[:12]}"
69
70
71
def default_dirty(files: int) -> DirtyShape:
72
    return DirtyShape(modified=max(1, files // 100), added=max(1, files // 400),
73
                      deleted=max(1, files // 1000), renamed=max(1, files // 1000))
74
75
76
def _tier(tier_id: str, kind: str, spec: GenSpec, **kwargs: Any) -> Tier:
77
    return Tier(tier_id=tier_id, kind=kind, spec=spec, dirty=kwargs.pop("dirty", default_dirty(spec.files)), **kwargs)
78
79
80
# Per-run clone/gc of a 1M-file or multi-GiB fixture costs tens of minutes on
81
# the dev host; XL axes time them in the calibrated campaign only.
82
XL_HEAVY = ("clone.full", "gc")
83
84
TIERS: Dict[str, Tier] = {
85
    "fresh": _tier("fresh", "tier", GenSpec(name="fresh", seed="oakbench-scale-fresh-v1", files=100,
86
                                            file_bytes=512, depth=3, fanout=16, commits=1, branches=1,
87
                                            branch_commit_files=2)),
88
    "default": _tier("default", "tier", GenSpec(name="default", seed="oakbench-scale-default-v1", files=5000,
89
                                                file_bytes=1024, depth=6, fanout=16, commits=1000,
90
                                                touches_per_commit=5, branches=50, tags=20)),
91
    "large": _tier("large", "tier", GenSpec(name="large", seed="oakbench-scale-large-v1", files=100_000,
92
                                            file_bytes=512, depth=8, fanout=64, commits=10_000,
93
                                            touches_per_commit=8, branches=1000, tags=200, binaries=1,
94
                                            binary_bytes=256 * MIB, binary_revisions=3)),
95
    "xl-history": _tier("xl-history", "xl", GenSpec(name="xl-history", seed="oakbench-scale-xl-history-v1",
96
                                                    files=10_000, file_bytes=256, depth=5, fanout=32,
97
                                                    commits=100_000, touches_per_commit=5, branches=1),
98
                        xl=True, skip_ops=XL_HEAVY),
99
    "xl-tree": _tier("xl-tree", "xl", GenSpec(name="xl-tree", seed="oakbench-scale-xl-tree-v1", files=1_000_000,
100
                                              file_bytes=64, depth=6, fanout=100, commits=3, branches=1),
101
                     dirty=DirtyShape(modified=1000, added=100, deleted=100, renamed=100), xl=True,
102
                     skip_ops=XL_HEAVY),
103
    "xl-deep": _tier("xl-deep", "xl", GenSpec(name="xl-deep", seed="oakbench-scale-xl-deep-v1", files=50_000,
104
                                              file_bytes=256, depth=40, fanout=8, commits=100, branches=1),
105
                     xl=True),
106
    "xl-refs": _tier("xl-refs", "xl", GenSpec(name="xl-refs", seed="oakbench-scale-xl-refs-v1", files=2000,
107
                                              file_bytes=512, depth=4, fanout=16, commits=1000,
108
                                              branches=10_000, tags=2000), xl=True),
109
    "xl-bigblob": _tier("xl-bigblob", "xl", GenSpec(name="xl-bigblob", seed="oakbench-scale-xl-bigblob-v1",
110
                                                    files=16, file_bytes=512, depth=2, fanout=16, commits=3,
111
                                                    binaries=1, binary_bytes=3 * GIB, binary_revisions=2,
112
                                                    branches=1),
113
                        xl=True, skip_ops=XL_HEAVY, sync=False),
114
    "xl-combined": _tier("xl-combined", "xl", GenSpec(name="xl-combined", seed="oakbench-scale-xl-combined-v1",
115
                                                      files=1_000_000, file_bytes=64, depth=32, fanout=100,
116
                                                      commits=100_000, touches_per_commit=5, branches=10_000,
117
                                                      tags=2000, binaries=1, binary_bytes=3 * GIB,
118
                                                      binary_revisions=2),
119
                         dirty=DirtyShape(modified=1000, added=100, deleted=100, renamed=100), xl=True,
120
                         combined=True, skip_ops=XL_HEAVY),
121
}
122
123
TIER_ORDER = ("fresh", "default", "large", "xl-history", "xl-tree", "xl-deep", "xl-refs", "xl-bigblob")
124
125
126
# ---------------------------------------------------------------------------
127
# The 14 core.yaml shapes
128
# ---------------------------------------------------------------------------
129
130
131
def load_core_shapes(path: Path = CORE_YAML) -> List[Dict[str, Any]]:
132
    import sys
133
    scripts = str(ROOT / "scripts")
134
    if scripts not in sys.path:
135
        sys.path.insert(0, scripts)
136
    from mount_probe import parse_yaml_subset  # the lanes' shared YAML-subset reader
137
    document = parse_yaml_subset(path.read_text())
138
    return list(document["scenarios"])
139
140
141
def _scaled(value: int, scale: float, minimum: int = 1) -> int:
142
    return max(minimum, int(math.ceil(value * scale))) if value else 0
143
144
145
def shape_tier(shape: Dict[str, Any], scale: float = 1.0) -> Tier:
146
    """A runnable Tier for one core.yaml scenario at ``scale``."""
147
    if not (0 < scale <= 1.0):
148
        raise ValueError("scale must be in (0, 1]")
149
    fs = shape["fixture_shape"]
150
    text = fs.get("text_files") or {}
151
    binary = fs.get("binary_files") or {}
152
    text_count = int(text.get("count") or 0)
153
    files = _scaled(max(1, text_count), scale)
154
    binaries = _scaled(int(binary.get("count") or 0), scale) if binary.get("count") else 0
155
    binary_bytes = int(binary.get("size_bytes") or 0)
156
    if binary_bytes and scale < 1.0:
157
        binary_bytes = max(MIB, int(binary_bytes * scale))
158
    commits = _scaled(int(fs.get("history_commits") or 1), scale)
159
    # at least one side branch so the switch/review/merge ops have a target
160
    branches = max(1, _scaled(int(fs.get("branches") or 1), scale) - 1)
161
    ignore = tuple(fs.get("ignore_patterns") or ())
162
    ignored = _scaled(int(fs.get("ignored_files") or 0), scale, minimum=0)
163
    if ignored and not ignore:
164
        ignore = ("build/",)
165
    conflict = fs.get("conflict_shape") or {}
166
    conflict_files = min(files, _scaled(int(conflict.get("conflicting_files") or 0), scale, minimum=0))
167
    binary_revisions = 0
168
    if binaries and shape["id"] == "binary-history":
169
        binary_revisions = commits
170
    dirty_raw = fs.get("dirty_shape") or {}
171
    dirty = DirtyShape(
172
        modified=max(1, _scaled(int(dirty_raw.get("modified_files") or 1), scale)),
173
        added=max(1, _scaled(int(dirty_raw.get("added_files") or 1), scale)),
174
        deleted=_scaled(int(dirty_raw.get("deleted_files") or 0), scale, minimum=0),
175
        renamed=_scaled(int(dirty_raw.get("renamed_files") or 0), scale, minimum=0),
176
    )
177
    # Dirty edits never exceed a quarter of the tree (the rest is protected
178
    # probe/branch/conflict paths and later runs' pools).
179
    cap = max(1, files // 4)
180
    dirty = DirtyShape(*(min(cap, value) for value in (dirty.modified, dirty.added, dirty.deleted, dirty.renamed)))
181
    spec = GenSpec(
182
        name=f"shape-{shape['id']}", seed=f"oakbench-core-shape-{shape['id']}-v1-x{scale:g}", files=files,
183
        file_bytes=max(32, int(text.get("size_bytes") or 512)), depth=max(1, int(fs.get("max_path_depth") or 2)),
184
        fanout=max(2, int(fs.get("path_fanout") or 16)), commits=commits,
185
        touches_per_commit=max(1, min(10, files // 10 or 1)), branches=branches,
186
        branch_commit_files=min(files, int((fs.get("branch_mutations") or {}).get("files_modified_per_branch") or 2)),
187
        binaries=binaries, binary_bytes=binary_bytes if binaries else 0, binary_revisions=binary_revisions,
188
        ignore_patterns=ignore, conflict_files=conflict_files,
189
        notes=f"core.yaml scenario {shape['id']} at scale {scale:g}",
190
    )
191
    return Tier(tier_id=f"shape-{shape['id']}-x{scale:g}", kind="shape", spec=spec, dirty=dirty,
192
                ignored_extras=ignored, notes=str(shape.get("purpose") or ""))
193
194
195
def core_shape_tiers(scale: float = 1.0, path: Path = CORE_YAML) -> Dict[str, Tier]:
196
    return {shape["id"]: shape_tier(shape, scale) for shape in load_core_shapes(path)}
197
198
199
def resolve(name: str, *, shape_scale: float = 1.0, allow_xl: bool = False,
200
            allow_xl_combined: bool = False) -> Tier:
201
    """Tier by name: a tier id, or ``shape:<core.yaml id>``."""
202
    if name.startswith("shape:"):
203
        shapes = core_shape_tiers(shape_scale)
204
        key = name.split(":", 1)[1]
205
        if key not in shapes:
206
            raise SystemExit(f"unknown core.yaml shape {key!r}; known: {', '.join(sorted(shapes))}")
207
        return shapes[key]
208
    if name not in TIERS:
209
        raise SystemExit(f"unknown tier {name!r}; known: {', '.join(TIERS)} or shape:<id>")
210
    tier = TIERS[name]
211
    if tier.xl and not allow_xl:
212
        raise SystemExit(f"tier {name} is XL: pass --allow-xl (and budget the disk; see suites/scale/README.md)")
213
    if tier.combined and not allow_xl_combined:
214
        raise SystemExit("xl-combined needs --allow-xl-combined as well as --allow-xl")
215
    return tier