kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

cc_bench_stages.py (6381B)


      1 #!/usr/bin/env python3
      2 """Attribute `sample` self-time to compiler STAGES for the cc benchmark.
      3 
      4 Reads build/bench/hot/<axis>/raw/<axis>.sample.txt (a macOS `sample` call graph),
      5 reconstructs flat self-time per function (same as cc_bench_report.py), then
      6 buckets each function into a pipeline stage by name/image so we can see where the
      7 time goes per stage rather than per function.
      8 """
      9 import os
     10 import re
     11 import sys
     12 from collections import defaultdict
     13 
     14 FRAME_RE = re.compile(r"^(?P<indent>[ |+!:]*)(?P<count>\d+)\s+(?P<rest>.*\S)\s*$")
     15 
     16 
     17 def parse_sample(text):
     18     lines = text.splitlines()
     19     start = None
     20     for i, ln in enumerate(lines):
     21         if ln.strip() == "Call graph:":
     22             start = i + 1
     23             break
     24     if start is None:
     25         return None, 0
     26     indents, counts, child_sum, names, stack = [], [], [], [], []
     27     started = False
     28     for ln in lines[start:]:
     29         if not ln.strip():
     30             if started:
     31                 break
     32             continue
     33         m = FRAME_RE.match(ln)
     34         if not m:
     35             if started:
     36                 break
     37             continue
     38         started = True
     39         indent = len(m.group("indent"))
     40         cnt = int(m.group("count"))
     41         rest = m.group("rest")
     42         if "  (in " in rest:
     43             sym = rest.split("  (in ", 1)[0].strip()
     44             img = rest.split("  (in ", 1)[1].split(")", 1)[0].strip()
     45         else:
     46             sym = rest.split("  ")[0].strip()
     47             img = ""
     48         idx = len(counts)
     49         indents.append(indent); counts.append(cnt); child_sum.append(0)
     50         names.append((sym, img))
     51         while stack and indent <= indents[stack[-1]]:
     52             stack.pop()
     53         if stack:
     54             child_sum[stack[-1]] += cnt
     55         stack.append(idx)
     56     self_by = defaultdict(float)
     57     total = 0.0
     58     for i in range(len(counts)):
     59         st = max(0, counts[i] - child_sum[i])
     60         sym, img = names[i]
     61         if not sym:
     62             continue
     63         self_by[(sym, img)] += st
     64         total += st
     65     return self_by, total
     66 
     67 
     68 # Stage classifier: ordered (first match wins). Each entry is (stage, predicate).
     69 def classify(sym, img):
     70     s = sym
     71     if "libsystem" in img or "dyld" in img.lower() or img.endswith(".dylib"):
     72         if "memset" in s or "memmove" in s or "bzero" in s or "memcpy" in s:
     73             return "libc:mem"
     74         if "write" in s or "read" in s or "mmap" in s or "munmap" in s or \
     75            "madvise" in s or "reclaim" in s or "fault" in s or "kevent" in s:
     76             return "libc:io/vm"
     77         return "libc:other"
     78     if s.startswith("DYLD-STUB"):
     79         return "libc:mem" if "mem" in s else "libc:other"
     80     # kit stages by symbol name
     81     LEX = ("lex_next", "peek", "bump", "scan_pp_number", "ucn_len",
     82            "skip_ws_and_comments", "lex_intern", "lex_fold_splices",
     83            "lex_sync_splices", "scan_quoted", "pp_number_is_float",
     84            "matches_include", "lex_here", "is_alnum", "is_alpha", "lex_loc")
     85     PP = ("pp_next", "src_next", "subst_phase", "substitute_body", "pp_emit",
     86           "push_buf", "src_push", "hs_add", "hs_register", "expand_", "preexpand",
     87           "read_invocation", "peek_for_invoke", "fetch_tok", "macro_lookup",
     88           "do_define", "do_directive", "pp_", "fdw_write", "w_str", "tv_grow",
     89           "tv_push", "kit_writer")
     90     INTERN = ("pool_intern", "fnv1a", "sym_eq", "kit_sym_intern", "kit_sym_str",
     91               "table_rehash", "pool_slice", "entries_grow")
     92     TYPES = ("type_ptr", "type_func", "type_array", "type_qualified",
     93              "type_unqual", "type_cg", "type_struct", "type_intern", "alloc_type",
     94              "alloc_struct", "c_abi", "abi_", "cache_get", "type_record",
     95              "type_compatible", "type_composite", "AbiInfoMap", "CgRecordMap",
     96              "TypeInternSet", "type_kind", "same_record", "type_promoted",
     97              "type_is_", "type_tag")
     98     PARSE = ("parse_", "declare_function", "scope_", "reject_", "expect_",
     99              "tag_", "external_func", "advance", "ident_kw", "SymEntryMap",
    100              "TagEntryMap", "ExternalFuncMap", "make_local", "p_", "perr",
    101              "decl_", "sem_")
    102     CG = ("nd_", "api_", "cg_", "native_", "kit_cg", "pcg_", "fold", "value",
    103           "arith", "abi_cg", "native_argmove", "make_sv")
    104     EMIT = ("aa_", "aa64_", "buf_", "obj_", "m_label", "emit_macho", "emit_",
    105             "mc_", "debug_emit", "debug_func", "wr_u32", "macho_", "elf_",
    106             "Sections_", "reloc", "obj_patch", "obj_write", "obj_pos",
    107             "section", "kit_debug", "x64_", "rv_")
    108     ARENA = ("arena_", "kit_arena", "c_pool", "heap_libc", "Pool", "buf_init")
    109 
    110     def has(prefixes):
    111         return any(s == p or s.startswith(p) for p in prefixes)
    112 
    113     if has(LEX): return "lexer"
    114     if has(INTERN): return "intern"
    115     if has(PP): return "pp"
    116     if has(TYPES): return "types/abi"
    117     if has(PARSE): return "parser"
    118     if has(CG): return "codegen"
    119     if has(EMIT): return "emit"
    120     if has(ARENA): return "arena/alloc"
    121     return "other:" + s
    122 
    123 
    124 def main():
    125     root = sys.argv[1] if len(sys.argv) > 1 else "build/bench/hot"
    126     axes = sys.argv[2:] if len(sys.argv) > 2 else sorted(
    127         d for d in os.listdir(root) if os.path.isdir(os.path.join(root, d)))
    128     for axis in axes:
    129         raw = os.path.join(root, axis, "raw", axis + ".sample.txt")
    130         if not os.path.exists(raw):
    131             continue
    132         text = open(raw, errors="replace").read()
    133         if text.startswith("skipped:") or "Call graph:" not in text:
    134             print(f"\n## {axis}: (no sample)")
    135             continue
    136         self_by, total = parse_sample(text)
    137         if not self_by or total <= 0:
    138             print(f"\n## {axis}: (unparseable)")
    139             continue
    140         stage = defaultdict(float)
    141         topfn = defaultdict(list)
    142         for (sym, img), v in self_by.items():
    143             st = classify(sym, img)
    144             stage[st] += v
    145             topfn[st].append((v, sym))
    146         print(f"\n## {axis}  ({int(total)} samples)")
    147         for st in sorted(stage, key=lambda k: stage[k], reverse=True):
    148             pct = 100.0 * stage[st] / total
    149             if pct < 0.5:
    150                 continue
    151             tops = sorted(topfn[st], reverse=True)[:3]
    152             tags = ", ".join("%s %.0f%%" % (n, 100.0 * c / total) for c, n in tops if c)
    153             print(f"  {pct:5.1f}%  {st:14s}  [{tags}]")
    154 
    155 
    156 if __name__ == "__main__":
    157     main()