summaryrefslogtreecommitdiff
path: root/build-support
diff options
context:
space:
mode:
Diffstat (limited to 'build-support')
-rw-r--r--build-support/capture-alloc-sources.py11
-rw-r--r--build-support/idea.py154
-rw-r--r--build-support/measure-alloc.py12
-rw-r--r--build-support/spill-matrix.py23
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")