diff options
Diffstat (limited to 'build-support')
| -rw-r--r-- | build-support/capture-alloc-sources.py | 11 | ||||
| -rw-r--r-- | build-support/idea.py | 154 | ||||
| -rw-r--r-- | build-support/measure-alloc.py | 12 | ||||
| -rw-r--r-- | build-support/spill-matrix.py | 23 |
4 files changed, 180 insertions, 20 deletions
diff --git a/build-support/capture-alloc-sources.py b/build-support/capture-alloc-sources.py index f474ac55..e12f9cc6 100644 --- a/build-support/capture-alloc-sources.py +++ b/build-support/capture-alloc-sources.py @@ -2,9 +2,10 @@ from pathlib import Path import subprocess,sys,os root=Path(__file__).resolve().parents[1] -ch=int(sys.argv[1]); chapter=root/f'chapter{ch}' -if ch not in range(21,25): raise SystemExit("Capture supports chapters 21-24") -out=root/'build/dynamic-stats'/str(ch); out.mkdir(parents=True,exist_ok=True) +name=sys.argv[1].removeprefix('chapter') +if name not in ('21','22','23','24b'): raise SystemExit("Capture supports chapters 21, 22, 23, 24b") +chapter=root/f'chapter{name}' +out=root/'build/dynamic-stats'/name; out.mkdir(parents=True,exist_ok=True) src=(chapter/'src/test/java/com/seaofnodes/simple/SpillStats.java').read_text() needle='String test = ACTIVE._test.getTestClass().getSimpleName()+"."+ACTIVE._test.getMethodName();' extra=''' @@ -15,7 +16,7 @@ extra=''' java.nio.file.Files.writeString(dir.resolve(name),code._src); java.nio.file.Files.writeString(dir.resolve("index.tsv"),cohort+"\\t"+test+"\\t"+cpu+"\\t"+abi+"\\t"+name+"\\n",java.nio.file.StandardOpenOption.CREATE,java.nio.file.StandardOpenOption.APPEND); } catch(java.io.IOException e) { throw new RuntimeException(e); } -'''.replace('CH',str(ch)) +'''.replace('CH',name) assert needle in src src=src.replace(needle,needle+extra).replace('private static SpillStats ACTIVE;','private static SpillStats ACTIVE;\n private static int _capture;') (out/'SpillStats.java').write_text(src,newline='\n') @@ -25,4 +26,4 @@ idx=out/'captured/index.tsv' if idx.exists(): idx.write_text('') with (out/'capture.log').open('w') as log: subprocess.run(['java','-ea','-cp',str(out)+os.pathsep+cp,'com.seaofnodes.simple.SpillStats'],cwd=chapter,stdout=log,stderr=subprocess.STDOUT,check=True) -print(ch,'captured',len(idx.read_text().splitlines())) +print(name,'captured',len(idx.read_text().splitlines())) diff --git a/build-support/idea.py b/build-support/idea.py new file mode 100644 index 00000000..84f75c22 --- /dev/null +++ b/build-support/idea.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python3 +"""Refresh the native IntelliJ project after adding or moving chapters. + +Close the project first: IDEA can otherwise save its old module list over ours. +No Maven import is needed. Personal settings stay in the ignored workspace.xml. +""" + +from pathlib import Path +import re +import shutil +import xml.etree.ElementTree as ET + +ROOT = Path(__file__).resolve().parent.parent + + +def save(path, text): + path = ROOT / path + data = text.encode("utf-8") + if path.exists() and path.read_bytes() == data: + return + if path.exists(): + backup = ROOT / "build/idea-backup" / path.relative_to(ROOT) + backup.parent.mkdir(parents=True, exist_ok=True) + # Keep the original settings across repeated refreshes. + if not backup.exists(): + shutil.copyfile(path, backup) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(data) + + +def xml(path, root): + ET.indent(root, space=" ") + save(path, '<?xml version="1.0" encoding="UTF-8"?>\n' + + ET.tostring(root, encoding="unicode") + "\n") + + +def module(name, directory, dependencies=(), junit=None, main="src/main/java"): + root = ET.Element("module", type="JAVA_MODULE", version="4") + manager = ET.SubElement(root, "component", name="NewModuleRootManager", + **{"inherit-compiler-output": "false"}) + relative = "../" * len(Path(directory).parts) + output = "file://$MODULE_DIR$/" + relative + "build/idea/" + ET.SubElement(manager, "output", url=output + "production/" + name) + ET.SubElement(manager, "output-test", url=output + "test/" + name) + ET.SubElement(manager, "exclude-output") + content = ET.SubElement(manager, "content", url="file://$MODULE_DIR$") + for source, test in [(main, "false"), ("src/test/java", "true")]: + if (ROOT / directory / source).is_dir(): + ET.SubElement(content, "sourceFolder", url="file://$MODULE_DIR$/" + source, + isTestSource=test) + for excluded in ["build", "target", "lib"] + (["src/test-support"] if name == "isa" else []): + ET.SubElement(content, "excludeFolder", url="file://$MODULE_DIR$/" + excluded) + ET.SubElement(manager, "orderEntry", type="inheritedJdk") + ET.SubElement(manager, "orderEntry", type="sourceFolder", forTests="false") + for dependency, scope in dependencies: + attributes = {"type": "module", "module-name": dependency} + if scope: + attributes["scope"] = scope + ET.SubElement(manager, "orderEntry", **attributes) + if junit: + entry = ET.SubElement(manager, "orderEntry", type="module-library", scope="TEST") + library = ET.SubElement(entry, "library", name="JUnit4") + classes = ET.SubElement(library, "CLASSES") + for jar in ["junit-4.12.jar", "hamcrest-core-1.3.jar"]: + ET.SubElement(classes, "root", url="jar://$MODULE_DIR$/" + junit + "/" + jar + "!/") + ET.SubElement(library, "JAVADOC") + ET.SubElement(library, "SOURCES") + path = directory + "/" + name + ".iml" + xml(path, root) + return path + + +def run(name, chapter, test_class=None, method=None): + root = ET.Element("component", name="ProjectRunConfigurationManager") + config = ET.SubElement(root, "configuration", default="false", name=name, + type="JUnit", factoryName="JUnit") + ET.SubElement(config, "module", name=chapter) + options = {"PACKAGE_NAME": "com.seaofnodes.simple", + "TEST_OBJECT": "method" if method else "package", + "VM_PARAMETERS": "-ea", "WORKING_DIRECTORY": "$PROJECT_DIR$/" + chapter} + if test_class: + options["MAIN_CLASS_NAME"] = "com.seaofnodes.simple." + test_class + if method: + options["METHOD_NAME"] = method + for key, value in options.items(): + ET.SubElement(config, "option", name=key, value=value) + scope = ET.SubElement(config, "TEST_SEARCH_SCOPE") + ET.SubElement(scope, "value", defaultName="singleModule") + before = ET.SubElement(config, "method", v="2") + ET.SubElement(before, "option", name="Make", enabled="true") + xml(".run/" + name + ".run.xml", root) + + +def main(): + chapters = sorted(path.name for path in ROOT.iterdir() + if re.fullmatch(r"chapter\d\d[a-z]?", path.name) + and (path / "pom.xml").is_file()) + paths = [] + for chapter in chapters: + dependencies = [("graph", None), ("print", None)] + if "isa/isa.mk" in (ROOT / chapter / "Makefile").read_text(encoding="utf-8"): + dependencies += [("isa", None), ("isa-eval", "TEST")] + paths.append(module(chapter, chapter, dependencies, "lib")) + run(chapter + " tests", chapter) + paths.append(module("print", "print")) + paths.append(module("graph", "graph", [("print", None)])) + paths.append(module("isa", "isa", [("isa-eval", "TEST")])) + paths.append(module("isa-eval", "isa/src/test-support", [("isa", None)], main="java")) + paths.append(module("transpile-tests", "transpile-tests", junit="../chapter01/lib")) + project = ET.Element("project", version="4") + manager = ET.SubElement(project, "component", name="ProjectModuleManager") + modules = ET.SubElement(manager, "modules") + for path in paths: + ET.SubElement(modules, "module", fileurl="file://$PROJECT_DIR$/" + path, + filepath="$PROJECT_DIR$/" + path) + xml(".idea/modules.xml", project) + run("chapter23 field write", "chapter23", "PrefixUpdateTest", "testMethodFieldWrites") + + # SDK names are local to the IDE installation. Preserve an existing choice; + # a new checkout asks the user to select an installed JDK 21 in Project Structure. + if not (ROOT / ".idea/misc.xml").exists(): + save(".idea/misc.xml", '<project version="4">\n' + ' <component name="ProjectRootManager" version="2" languageLevel="JDK_21">\n' + ' <output url="file://$PROJECT_DIR$/build/idea" />\n' + ' </component>\n</project>\n') + + # New gutter runs must use the selected module's directory, not the repo root. + # Do not rewrite existing personal configurations, breakpoints or window state. + workspace = ROOT / ".idea/workspace.xml" + project = ET.parse(workspace).getroot() if workspace.exists() else ET.Element("project", version="4") + manager = project.find("component[@name='RunManager']") + if manager is None: + manager = ET.SubElement(project, "component", name="RunManager") + template = manager.find("configuration[@default='true'][@type='JUnit']") + if template is None: + template = ET.SubElement(manager, "configuration", default="true", type="JUnit", factoryName="JUnit") + option = template.find("option[@name='WORKING_DIRECTORY']") + if option is None: + option = ET.SubElement(template, "option", name="WORKING_DIRECTORY") + option.set("value", "$MODULE_WORKING_DIR$") + if template.find("option[@name='VM_PARAMETERS']") is None: + ET.SubElement(template, "option", name="VM_PARAMETERS", value="-ea") + if template.find("TEST_SEARCH_SCOPE") is None: + scope = ET.SubElement(template, "TEST_SEARCH_SCOPE") + ET.SubElement(scope, "value", defaultName="singleModule") + if template.find("method") is None: + before = ET.SubElement(template, "method", v="2") + ET.SubElement(before, "option", name="Make", enabled="true") + xml(".idea/workspace.xml", project) + print(f"Configured {len(chapters)} chapters and 5 shared/tool modules. Select JDK 21 and reopen the project.") + + +if __name__ == "__main__": + main() diff --git a/build-support/measure-alloc.py b/build-support/measure-alloc.py index ed899713..c563eea7 100644 --- a/build-support/measure-alloc.py +++ b/build-support/measure-alloc.py @@ -1,9 +1,11 @@ """Compile the shared measurement harness against one chapter's existing classes.""" from pathlib import Path import json,os,re,subprocess,sys -root=Path(__file__).resolve().parents[1]; ch=int(sys.argv[1]); chapter=root/f'chapter{ch}' -if ch not in range(21,26): raise SystemExit("Dynamic measurement supports chapters 21-25") -out=root/'build/dynamic-stats'/str(ch);out.mkdir(parents=True,exist_ok=True) +root=Path(__file__).resolve().parents[1]; name=sys.argv[1].removeprefix('chapter') +slots={'21':21,'22':22,'23':23,'24b':24,'28b':25} +if name not in slots: raise SystemExit("Dynamic measurement supports chapters 21, 22, 23, 24b, 28b") +ch=slots[name]; chapter=root/f'chapter{name}' +out=root/'build/dynamic-stats'/name;out.mkdir(parents=True,exist_ok=True) src=(root/'build-support/AllocMetrics.java.in').read_text() subs={'CHAPTER':str(ch),'OPSTART':'e.opStart(n)' if ch==25 else 'e._opStart[n._nid]', 'OPLEN':'e.opLen(n)' if ch==25 else 'e._opLen[n._nid]', @@ -26,7 +28,7 @@ cp=os.pathsep.join([str(chapter/'build/classes/main'),str(chapter/'build/classes subprocess.run(['javac','-cp',cp,'-d',str(out),str(out/'Measure.java')],check=True) subprocess.run(['javac','-cp',cp,'-d',str(out),*[str(p) for p in (root/'isa/src/test-support/java/com/seaofnodes/isa/eval').glob('*.java')]],check=True) if ch==25: - p=root/'chapter25/src/test/java/com/seaofnodes/simple/spill' + p=root/'chapter28b/src/test/java/com/seaofnodes/simple/spill' index=out/'index.tsv' rows=[] for line in (p/'cohorts.tsv').read_text().splitlines(): @@ -42,4 +44,4 @@ if ch==25: else:index=out/'captured/index.tsv' with (out/'dynamic.log').open('w') as log: subprocess.run(['java','-ea','-cp',str(out)+os.pathsep+cp,'Measure',str(index)],cwd=chapter,stdout=log,stderr=subprocess.STDOUT,check=True) -print(ch, 'done') +print(name, 'done') diff --git a/build-support/spill-matrix.py b/build-support/spill-matrix.py index 9ee8f7eb..bd11c447 100644 --- a/build-support/spill-matrix.py +++ b/build-support/spill-matrix.py @@ -9,6 +9,9 @@ import sys ROOT = Path(__file__).resolve().parent.parent CHAPTERS = range(20, 26) +# Cohort IDs are historical test identities. Compiler directories follow the +# final teaching sequence; never silently reuse logs from the retired 24/25. +COMPILERS = {c: str(c) for c in CHAPTERS} | {24: "24b", 25: "28b"} START = "<!-- spill-matrix:start -->" END = "<!-- spill-matrix:end -->" @@ -21,7 +24,7 @@ def read_log(path): def read_results(log_dir): results = {} for chapter in CHAPTERS: - path = log_dir / f"spill-matrix-{chapter}.log" + path = log_dir / f"spill-matrix-{COMPILERS[chapter]}.log" text = read_log(path) if re.search(r"FAILED|(?:[Ff]ailures|Spill golden failures): [1-9]", text): raise ValueError(f"Chapter {chapter} failed; see {path}") @@ -65,7 +68,7 @@ def read_dynamic(results): values = {} records = [] for chapter in range(21, 26): - path = ROOT / f"build/dynamic-stats/{chapter}/dynamic.log" + path = ROOT / f"build/dynamic-stats/{COMPILERS[chapter]}/dynamic.log" text = read_log(path) if "FAILED," in text: raise ValueError(f"Dynamic measurement failed; see {path}") @@ -109,7 +112,7 @@ def read_dynamic(results): def table(results, dynamic, last=25): chapters = range(20, last + 1) - lines = ["| Test cohort | Cases D / X | " + " | ".join(f"Ch {c}: Ops / RA / X" for c in chapters) + " |", + lines = ["| Historical test cohort | Cases D / X | " + " | ".join(f"Ch {COMPILERS[c]}: Ops / RA / X" for c in chapters) + " |", "|---|---:|" + "---:|" * len(chapters)] for cohort in chapters: count = len(dynamic[max(21,cohort)][cohort]) @@ -141,29 +144,29 @@ def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--log-dir", type=Path, default=ROOT / "build/spill-stats") parser.add_argument("--reuse", action="store_true", help="read completed logs without rerunning tests") - parser.add_argument("--update-docs", action="store_true", help="refresh the marked tables in docs and READMEs") + parser.add_argument("--update-docs", action="store_true", help="refresh the allocator report in skills/current-spill-matrix.md") args = parser.parse_args() args.log_dir.mkdir(parents=True, exist_ok=True) if not args.reuse: for chapter in CHAPTERS: - path = args.log_dir / f"spill-matrix-{chapter}.log" + path = args.log_dir / f"spill-matrix-{COMPILERS[chapter]}.log" print(f"Measuring chapter {chapter}: {path}", flush=True) with path.open("wb") as log: - run = subprocess.run(["make", "-C", str(ROOT / f"chapter{chapter}"), "spill-stats"], + run = subprocess.run(["make", "-C", str(ROOT / f"chapter{COMPILERS[chapter]}"), "spill-stats"], stdout=log, stderr=subprocess.STDOUT) if run.returncode: raise ValueError(f"Chapter {chapter} failed (exit {run.returncode}); see {path}") for chapter in range(21, 26): if chapter < 25: - subprocess.run([sys.executable, str(ROOT / "build-support/capture-alloc-sources.py"), str(chapter)], check=True) - subprocess.run([sys.executable, str(ROOT / "build-support/measure-alloc.py"), str(chapter)], check=True) + subprocess.run([sys.executable, str(ROOT / "build-support/capture-alloc-sources.py"), COMPILERS[chapter]], check=True) + subprocess.run([sys.executable, str(ROOT / "build-support/measure-alloc.py"), COMPILERS[chapter]], check=True) results = read_results(args.log_dir) dynamic = read_dynamic(results) matrix = table(results, dynamic) (args.log_dir / "matrix.md").write_text(matrix + "\n", encoding="utf-8", newline="\n") if args.update_docs: - paths = [(ROOT / "docs/regalloc-spills.md", matrix)] - paths += [(ROOT / f"chapter{c}/README.md", table(results, dynamic, c)) for c in CHAPTERS] + # Preserve the dated pre-split measurements in the original READMEs. + paths = [(ROOT / "skills/current-spill-matrix.md", matrix)] # Validate all destinations before changing any document. for path, _ in paths: text = path.read_text(encoding="utf-8") |
