Files
browser/orderfile/tools/gen_order.py
T
Karl Seguin c8abf899ef mem: include v8 functions in binary layout optimization
https://github.com/lightpanda-io/browser/pull/3271 introduce an orderfile so
that hot sections of code were grouped together in the binary, resulting in more
efficient loading. But, it excluded v8 functions because v8 is compiled with
`-fno-unique-section-names` so each section gets the same name. Because of this
the orderfile can't target specific "hot" or "cold" functions.

As a follow up to 3271, I thought we'd be able to re-compile v8 without that
flag, but:

1 - The flag isn't directly exposed by v8, so we'd need to change v8's own build
    BUILD.gn in our build process
2 - It would make the .a file ~ 40MB larger (though the final lightpanda binary
    would stay the same size
3 - If we wanted to created two .a files (one with -fno-unique-section-names and
    one without), it would require a full rebuild (for both x86-64 and arm).

I didn't love those compromises, so I asked Claude if the sections names could
be generated in a separate file and then that could be used when generating out
lightpanda.ld. What claude came up with was a small Zig script
(mark_hot_sections.zig) which is now run as part of the build. It takes the
v8.a file (which still has `-fno-unique-section-names`), it takes a text list
of hot functions, and it re-generates v8.a with a special .text.hot and
.rodata.hot sections. lightpanda.ld can include these.

This PR depends on https://github.com/lightpanda-io/zig-v8-fork/pull/202 but
202 doesn't require a v8 rebuild. It merely allows prebuilt_v8_path to be
a lazypath, which we need because the prebuilt_v8_path that we pass is now
generated from this build script.
2026-08-26 12:53:45 +08:00

77 lines
3.9 KiB
Python
Executable File

#!/usr/bin/env python3
"""usage: gen_order.py <hot.text> <hot.rodata> <out.ld> [--v8 <libc_v8.a> <v8.txt>] <obj-or-archive>...
Builds a symbol -> (file, section) map from the objects and emits an INSERT
linker script that places the hot sections in .text.hot / .rodata.hot ahead of
.text / .rodata. Patterns are scoped to their object file (`*api.o(...)`):
LLD tests every input section against every unscoped pattern, which turns a
26k-pattern script into an 80s link; scoped, it is a few seconds.
With --v8, symbols defined in that archive are written to <v8.txt> for
mark_hot_sections.zig instead, and the script matches them with one
`.text.hot.*` / `.rodata.hot.*` glob (V8's sections are all named `.text`).
"""
import sys, subprocess, collections, re, os
hot_text, hot_rodata, out = sys.argv[1:4]
objs = sys.argv[4:]
v8_archive = v8_out = None
if objs and objs[0] == "--v8":
v8_archive, v8_out = os.path.basename(objs[1]), objs[2]
objs = [objs[1]] + objs[3:]
loc_of = collections.defaultdict(set) # symbol -> {(archive, file, section)}
for o in objs:
p = subprocess.run(["objdump", "-t", o], capture_output=True, text=True).stdout
fname = archive = os.path.basename(o)
for line in p.splitlines():
m = re.match(r"^(.+?):\s+file format elf", line)
if m:
fname = os.path.basename(m.group(1))
continue
# value flags section<TAB>size name -- the section can contain spaces (zig)
if "\t" not in line: continue
left, right = line.split("\t", 1)
parts = right.split(None, 1)
if len(parts) != 2: continue
name = parts[1].strip()
for vis in (".hidden ", ".protected ", ".internal "):
if name.startswith(vis): name = name[len(vis):]
fl = left[17:24]
if "d" in fl: continue # section symbol
sec = left[24:].strip()
if sec.startswith((".text", ".rodata")):
loc_of[name].add((archive, fname, sec))
def esc(s):
return re.sub(r'([*?\[\]\\])', r'\\\1', s)
stats = collections.Counter()
v8_syms = []
def emit(hotfile, prefix):
by_file = collections.OrderedDict(); seen = set()
for name in open(hotfile).read().split("\n"):
if not name: continue
locs = loc_of.get(name)
if not locs: stats[prefix + " nomap"] += 1; continue
for archive, fname, sec in sorted(locs):
if not sec.startswith(prefix): continue
if archive == v8_archive:
# The embedded builtins blob is left cold (see
# mark_hot_sections.zig); don't emit a pattern for it.
if name.startswith("Builtins_"): stats[prefix + " v8-blob-skip"] += 1; continue
v8_syms.append(name); stats[prefix + " v8"] += 1; continue
if sec == prefix or sec.endswith("."): stats[prefix + " generic"] += 1; continue
if '"' in sec or '"' in fname: stats[prefix + " quote-skip"] += 1; continue
if (fname, sec) in seen: continue
seen.add((fname, sec)); by_file.setdefault(fname, []).append(sec)
stats[prefix + " sections"] = len(seen); stats[prefix + " files"] = len(by_file)
# LLD unquotes section names but not the file pattern, so that one stays bare.
lines = [f' *{esc(f)}(' + " ".join(f'"{esc(s)}"' for s in secs) + ")" for f, secs in by_file.items()]
if v8_archive: lines.insert(0, f' *("{prefix}.hot.*")')
return lines
t = emit(hot_text, ".text"); r = emit(hot_rodata, ".rodata")
with open(out, "w") as f:
f.write("/* Generated by orderfile/tools/gen_order.py, see orderfile/README.md. */\n")
f.write("SECTIONS {\n .text.hot : {\n" + "\n".join(t) + "\n }\n} INSERT BEFORE .text;\n")
f.write("SECTIONS {\n .rodata.hot : {\n" + "\n".join(r) + "\n }\n} INSERT BEFORE .rodata;\n")
if v8_out:
with open(v8_out, "w") as f: f.write("\n".join(dict.fromkeys(v8_syms)) + "\n")
for k, v in sorted(stats.items()): print(f"{k}: {v}")