From 54924d21c7939f51969382fa5e1e1baec8ba8e8c Mon Sep 17 00:00:00 2001 From: Abul Date: Tue, 21 Jul 2026 22:13:46 +0530 Subject: [PATCH 1/4] adding ftime --- .github/scripts/perf_comment.py | 35 +++++++++++++++-- tools/perfrunner/source/app.d | 2 +- tools/perfrunner/source/metrics.d | 13 ++++++- tools/perfrunner/source/report.d | 14 ++++--- tools/perfrunner/source/timetrace.d | 60 +++++++++++++++++++++++++++++ 5 files changed, 114 insertions(+), 10 deletions(-) create mode 100644 tools/perfrunner/source/timetrace.d diff --git a/.github/scripts/perf_comment.py b/.github/scripts/perf_comment.py index 8caec894d2fa..c4586927243e 100644 --- a/.github/scripts/perf_comment.py +++ b/.github/scripts/perf_comment.py @@ -32,6 +32,30 @@ def fmt_delta(pct): return f"{sign}{value}%" +def fmt_share(value, total): + if total <= 0: + return "n/a" + return f"{value / total * 100:.0f}%" + + +def is_significant(m): + return abs(m["delta_pct"]) >= THRESHOLDS.get(m["method"], 0.1) + + +# Stage rows (frontend/backend) shown as base%/head% shares of their group. +def breakdown(children): + base_total = sum(c["base"] for c in children) + head_total = sum(c["head"] for c in children) + if head_total <= 0: + return [] + return ["| ↳ {} | {} | {} | {} |".format( + c["label"], + fmt_share(c["base"], base_total), + fmt_share(c["head"], head_total), + fmt_delta(c["delta_pct"]), + ) for c in children] + + def render(results): lines = [ MARKER, @@ -40,13 +64,19 @@ def render(results): "| Metric | Base | PR | delta |", "|--------|------|----|-------|", ] - for m in results["metrics"]: + metrics = results["metrics"] + for m in metrics: + if m.get("parent"): + continue lines.append("| {} | {} | {} | {} |".format( m["label"], fmt_value(m["base"], m["unit"]), fmt_value(m["head"], m["unit"]), fmt_delta(m["delta_pct"]), )) + children = [c for c in metrics if c.get("parent") == m["id"]] + if children and is_significant(m): + lines += breakdown(children) return "\n".join(lines) + "\n" @@ -89,8 +119,7 @@ def main(): return significant = any( - abs(m["delta_pct"]) >= THRESHOLDS.get(m["method"], 0.1) - for m in results["metrics"] + is_significant(m) for m in results["metrics"] if not m.get("parent") ) if not significant: print("all deltas within noise threshold, skipping comment") diff --git a/tools/perfrunner/source/app.d b/tools/perfrunner/source/app.d index 98830374a4f6..5a7cd86489c3 100644 --- a/tools/perfrunner/source/app.d +++ b/tools/perfrunner/source/app.d @@ -55,7 +55,7 @@ int main(string[] args) MetricResult[] metrics; foreach (def; initials) - metrics ~= MetricResult(def.id, def.label, def.unit, def.method, + metrics ~= MetricResult(def.id, def.label, def.unit, def.method, def.parent, base[def.id], head[def.id]); auto rep = Report(baseSha, "merge-base", headSha, pr, os, hostDmd, metrics); diff --git a/tools/perfrunner/source/metrics.d b/tools/perfrunner/source/metrics.d index f22cdcdf5690..96a62e22da07 100644 --- a/tools/perfrunner/source/metrics.d +++ b/tools/perfrunner/source/metrics.d @@ -8,6 +8,7 @@ import std.regex : ctRegex, matchFirst; import std.process : execute; import cachegrind : instructions; +import timetrace : stages; struct MetricDef { @@ -15,6 +16,7 @@ struct MetricDef string label; string unit; string method; + string parent; // headline metric this row breaks down, empty for top-level rows } // Some initial metrics to measure will add more later @@ -26,6 +28,8 @@ immutable MetricDef[] initials = [ MetricDef("hello_binary_size", "hello binary size", "bytes", "stat"), MetricDef("hello_max_rss", "peak RSS (compile hello.d)", "kb", "time -v"), MetricDef("phobos_max_rss", "peak RSS (compile Phobos)", "kb", "time -v"), + MetricDef("phobos_stage_frontend", "frontend (parse+sema)", "us", "time-trace", "compile_phobos_instr"), + MetricDef("phobos_stage_backend", "backend (inline+codegen)", "us", "time-trace", "compile_phobos_instr"), ]; // Measure every metric for one dmd binary. `tag` ("base"/"head") @@ -34,7 +38,7 @@ long[string] measure(string dmd, string workload, string phobos, string tmp, str { auto stdPackage = buildPath(phobos, "std", "package.d"); auto phobosFlags = ["-i=std", "-preview=dip1000"]; - return [ + long[string] m = [ "compile_hello_debug_instr": instructions(dmd, [], workload, tmp, tag ~ "-dbg"), "compile_hello_release_instr": instructions(dmd, ["-O", "-release"], workload, tmp, tag ~ "-rel"), "compile_phobos_instr": instructions(dmd, phobosFlags, stdPackage, tmp, tag ~ "-phobos"), @@ -43,6 +47,13 @@ long[string] measure(string dmd, string workload, string phobos, string tmp, str "hello_max_rss": maxRss(dmd, [], workload, tmp, tag), "phobos_max_rss": maxRss(dmd, phobosFlags, stdPackage, tmp, tag ~ "-phobos"), ]; + + // Stage breakdown of the Phobos compile. Zero on a base too old for + // -ftime-trace; the comment renders that side as n/a. + auto st = stages(dmd, phobosFlags, stdPackage, tmp, tag ~ "-phobos"); + m["phobos_stage_frontend"] = st.get("stage_frontend_us", 0); + m["phobos_stage_backend"] = st.get("stage_backend_us", 0); + return m; } // Byte size of `binary` diff --git a/tools/perfrunner/source/report.d b/tools/perfrunner/source/report.d index a28e7217a086..8d7638b1d1bb 100644 --- a/tools/perfrunner/source/report.d +++ b/tools/perfrunner/source/report.d @@ -11,6 +11,7 @@ struct MetricResult string label; string unit; string method; + string parent; long base; long head; } @@ -32,7 +33,7 @@ string render(Report rep) JSONValue[] metrics; foreach (m; rep.metrics) { - metrics ~= JSONValue([ + JSONValue[string] obj = [ "id": JSONValue(m.id), "label": JSONValue(m.label), "unit": JSONValue(m.unit), @@ -40,11 +41,14 @@ string render(Report rep) "base": JSONValue(m.base), "head": JSONValue(m.head), "delta_pct": JSONValue(round(deltaPct(m.base, m.head) * 100) / 100.0), - ]); + ]; + if (m.parent.length) + obj["parent"] = JSONValue(m.parent); + metrics ~= JSONValue(obj); } JSONValue root = [ - "schema_version": JSONValue(1), + "schema_version": JSONValue(2), "base": JSONValue(["sha": JSONValue(rep.baseSha), "ref": JSONValue(rep.baseRef)]), "head": JSONValue(["sha": JSONValue(rep.headSha), "pr": JSONValue(rep.pr)]), "runner": JSONValue(["os": JSONValue(rep.os), "host_dmd": JSONValue(rep.hostDmd)]), @@ -58,10 +62,10 @@ unittest { auto rep = Report("base1", "merge-base", "head1", 7, "ubuntu-latest", "2.112.0", [MetricResult("compile_hello_debug_instr", "compile hello.d (instr)", - "count", "cachegrind", 1000, 1010)]); + "count", "cachegrind", "", 1000, 1010)]); auto j = parseJSON(render(rep)); - assert(j["schema_version"].integer == 1); + assert(j["schema_version"].integer == 2); assert(j["base"]["sha"].str == "base1"); assert(j["head"]["pr"].integer == 7); assert(j["metrics"].array.length == 1); diff --git a/tools/perfrunner/source/timetrace.d b/tools/perfrunner/source/timetrace.d new file mode 100644 index 000000000000..b637c5d15579 --- /dev/null +++ b/tools/perfrunner/source/timetrace.d @@ -0,0 +1,60 @@ +module timetrace; + +import std.algorithm : canFind; +import std.file : readText; +import std.json : parseJSON; +import std.path : buildPath; +import std.process : execute; + +// The top-level -ftime-trace spans grouped into frontend/backend. These names +// match main.d's generic phase spans; they don't overlap, so summing them +// partitions the compile without double-counting the nested per-symbol events. +private immutable string[] frontendSpans = ["Parsing", "Semantic analysis"]; +private immutable string[] backendSpans = ["Inlining", "Code generation"]; + +// Sum the duration (microseconds) of the top-level spans in each bucket. +long[string] parseStages(string trace) +{ + long frontend, backend; + foreach (e; parseJSON(trace)["traceEvents"].array) + { + if (e["ph"].str != "X") + continue; + auto name = e["name"].str; + if (frontendSpans.canFind(name)) + frontend += e["dur"].integer; + else if (backendSpans.canFind(name)) + backend += e["dur"].integer; + } + return ["stage_frontend_us": frontend, "stage_backend_us": backend]; +} + +// Compile the workload with -ftime-trace and read the per-stage durations. +// Returns null if the compiler is too old to know the flag (older base side). +long[string] stages(string dmd, string[] dflags, string workload, string tmp, string tag) +{ + auto obj = buildPath(tmp, tag ~ "-tt.o"); + auto trace = buildPath(tmp, tag ~ ".time-trace"); + auto cmd = [dmd, "-c", "-ftime-trace", "-ftime-trace-file=" ~ trace] + ~ dflags ~ [workload, "-of=" ~ obj]; + if (execute(cmd).status != 0) + return null; + return parseStages(readText(trace)); +} + +unittest +{ + auto sample = `{"traceEvents": [ + {"ph":"M","name":"process_name"}, + {"ph":"X","name":"Parsing","dur":100}, + {"ph":"X","name":"Parse: Module foo","dur":40}, + {"ph":"X","name":"Semantic analysis","dur":300}, + {"ph":"X","name":"Sema1: Function bar","dur":50}, + {"ph":"X","name":"Inlining","dur":20}, + {"ph":"X","name":"Code generation","dur":80}, + {"ph":"X","name":"Codegen: function bar","dur":30} + ]}`; + auto s = parseStages(sample); + assert(s["stage_frontend_us"] == 400); + assert(s["stage_backend_us"] == 100); +} From a7a69d13d6a73334770497477c02d024f2f0e890 Mon Sep 17 00:00:00 2001 From: Abul Date: Tue, 21 Jul 2026 22:35:59 +0530 Subject: [PATCH 2/4] test the time --- compiler/src/dmd/expressionsem.d | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/compiler/src/dmd/expressionsem.d b/compiler/src/dmd/expressionsem.d index 3b679736c3a0..bcb7222ae9ce 100644 --- a/compiler/src/dmd/expressionsem.d +++ b/compiler/src/dmd/expressionsem.d @@ -15808,6 +15808,16 @@ Expression expressionSemantic(Expression e, Scope* sc) if (e.expressionSemanticDone) return e; + // TEMP: deliberate frontend slowdown to exercise the perf bot's stage + // breakdown. Remove before merging. Bump the count if the delta is too + // small to cross the threshold. + { + import core.volatile : volatileLoad, volatileStore; + __gshared uint sink; + foreach (i; 0 .. 50) + volatileStore(&sink, volatileLoad(&sink) + i); + } + scope v = new ExpressionSemanticVisitor(sc); e.accept(v); return v.result; From 85e8f60f962ed9b9fa6374caf2fca30ce88aa6f5 Mon Sep 17 00:00:00 2001 From: Abul Date: Wed, 22 Jul 2026 18:34:15 +0530 Subject: [PATCH 3/4] refining stage breakdown --- .github/scripts/perf_comment.py | 25 ++++++++++++++++------ compiler/src/dmd/expressionsem.d | 10 --------- tools/perfrunner/source/metrics.d | 8 +++++++ tools/perfrunner/source/timetrace.d | 33 ++++++++++++++++++++--------- 4 files changed, 49 insertions(+), 27 deletions(-) diff --git a/.github/scripts/perf_comment.py b/.github/scripts/perf_comment.py index c4586927243e..659421ae0818 100644 --- a/.github/scripts/perf_comment.py +++ b/.github/scripts/perf_comment.py @@ -42,18 +42,29 @@ def is_significant(m): return abs(m["delta_pct"]) >= THRESHOLDS.get(m["method"], 0.1) -# Stage rows (frontend/backend) shown as base%/head% shares of their group. +# Stage rows (frontend/backend) shown as base%/head% shares with pp delta. def breakdown(children): base_total = sum(c["base"] for c in children) head_total = sum(c["head"] for c in children) if head_total <= 0: return [] - return ["| ↳ {} | {} | {} | {} |".format( - c["label"], - fmt_share(c["base"], base_total), - fmt_share(c["head"], head_total), - fmt_delta(c["delta_pct"]), - ) for c in children] + rows = [] + for c in children: + base_pct = c["base"] / base_total * 100 if base_total > 0 else 0 + head_pct = c["head"] / head_total * 100 if head_total > 0 else 0 + pp = head_pct - base_pct + if abs(pp) < 0.5: + delta_str = "~0pp" + else: + sign = "+" if pp > 0 else "" + delta_str = f"{sign}{pp:.0f}pp" + rows.append("| ↳ {} | {} | {} | {} |".format( + c["label"], + fmt_share(c["base"], base_total), + fmt_share(c["head"], head_total), + delta_str, + )) + return rows def render(results): diff --git a/compiler/src/dmd/expressionsem.d b/compiler/src/dmd/expressionsem.d index bcb7222ae9ce..3b679736c3a0 100644 --- a/compiler/src/dmd/expressionsem.d +++ b/compiler/src/dmd/expressionsem.d @@ -15808,16 +15808,6 @@ Expression expressionSemantic(Expression e, Scope* sc) if (e.expressionSemanticDone) return e; - // TEMP: deliberate frontend slowdown to exercise the perf bot's stage - // breakdown. Remove before merging. Bump the count if the delta is too - // small to cross the threshold. - { - import core.volatile : volatileLoad, volatileStore; - __gshared uint sink; - foreach (i; 0 .. 50) - volatileStore(&sink, volatileLoad(&sink) + i); - } - scope v = new ExpressionSemanticVisitor(sc); e.accept(v); return v.result; diff --git a/tools/perfrunner/source/metrics.d b/tools/perfrunner/source/metrics.d index 96a62e22da07..888a980a91e1 100644 --- a/tools/perfrunner/source/metrics.d +++ b/tools/perfrunner/source/metrics.d @@ -30,6 +30,10 @@ immutable MetricDef[] initials = [ MetricDef("phobos_max_rss", "peak RSS (compile Phobos)", "kb", "time -v"), MetricDef("phobos_stage_frontend", "frontend (parse+sema)", "us", "time-trace", "compile_phobos_instr"), MetricDef("phobos_stage_backend", "backend (inline+codegen)", "us", "time-trace", "compile_phobos_instr"), + MetricDef("phobos_stage_parse", "parse", "us", "time-trace", "phobos_stage_frontend"), + MetricDef("phobos_stage_sema", "semantic analysis", "us", "time-trace", "phobos_stage_frontend"), + MetricDef("phobos_stage_inline", "inlining", "us", "time-trace", "phobos_stage_backend"), + MetricDef("phobos_stage_codegen", "code generation", "us", "time-trace", "phobos_stage_backend"), ]; // Measure every metric for one dmd binary. `tag` ("base"/"head") @@ -53,6 +57,10 @@ long[string] measure(string dmd, string workload, string phobos, string tmp, str auto st = stages(dmd, phobosFlags, stdPackage, tmp, tag ~ "-phobos"); m["phobos_stage_frontend"] = st.get("stage_frontend_us", 0); m["phobos_stage_backend"] = st.get("stage_backend_us", 0); + m["phobos_stage_parse"] = st.get("stage_parse_us", 0); + m["phobos_stage_sema"] = st.get("stage_sema_us", 0); + m["phobos_stage_inline"] = st.get("stage_inline_us", 0); + m["phobos_stage_codegen"] = st.get("stage_codegen_us", 0); return m; } diff --git a/tools/perfrunner/source/timetrace.d b/tools/perfrunner/source/timetrace.d index b637c5d15579..fa63708edf81 100644 --- a/tools/perfrunner/source/timetrace.d +++ b/tools/perfrunner/source/timetrace.d @@ -1,6 +1,6 @@ module timetrace; -import std.algorithm : canFind; + import std.file : readText; import std.json : parseJSON; import std.path : buildPath; @@ -9,24 +9,33 @@ import std.process : execute; // The top-level -ftime-trace spans grouped into frontend/backend. These names // match main.d's generic phase spans; they don't overlap, so summing them // partitions the compile without double-counting the nested per-symbol events. -private immutable string[] frontendSpans = ["Parsing", "Semantic analysis"]; -private immutable string[] backendSpans = ["Inlining", "Code generation"]; - // Sum the duration (microseconds) of the top-level spans in each bucket. +// Returns both coarse (frontend/backend) and fine (parse/sema/inline/codegen). long[string] parseStages(string trace) { - long frontend, backend; + long parse, sema, inline, codegen; foreach (e; parseJSON(trace)["traceEvents"].array) { if (e["ph"].str != "X") continue; auto name = e["name"].str; - if (frontendSpans.canFind(name)) - frontend += e["dur"].integer; - else if (backendSpans.canFind(name)) - backend += e["dur"].integer; + if (name == "Parsing") + parse += e["dur"].integer; + else if (name == "Semantic analysis") + sema += e["dur"].integer; + else if (name == "Inlining") + inline += e["dur"].integer; + else if (name == "Code generation") + codegen += e["dur"].integer; } - return ["stage_frontend_us": frontend, "stage_backend_us": backend]; + return [ + "stage_frontend_us": parse + sema, + "stage_backend_us": inline + codegen, + "stage_parse_us": parse, + "stage_sema_us": sema, + "stage_inline_us": inline, + "stage_codegen_us": codegen, + ]; } // Compile the workload with -ftime-trace and read the per-stage durations. @@ -57,4 +66,8 @@ unittest auto s = parseStages(sample); assert(s["stage_frontend_us"] == 400); assert(s["stage_backend_us"] == 100); + assert(s["stage_parse_us"] == 100); + assert(s["stage_sema_us"] == 300); + assert(s["stage_inline_us"] == 20); + assert(s["stage_codegen_us"] == 80); } From eb379beab38a89a370578b11759524348de54a28 Mon Sep 17 00:00:00 2001 From: Abul Date: Wed, 22 Jul 2026 18:36:17 +0530 Subject: [PATCH 4/4] Introduce temporary frontend slowdown for performance testing --- compiler/src/dmd/expressionsem.d | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/compiler/src/dmd/expressionsem.d b/compiler/src/dmd/expressionsem.d index 3b679736c3a0..92e86899eb73 100644 --- a/compiler/src/dmd/expressionsem.d +++ b/compiler/src/dmd/expressionsem.d @@ -15808,6 +15808,16 @@ Expression expressionSemantic(Expression e, Scope* sc) if (e.expressionSemanticDone) return e; + // TEMP: deliberate frontend slowdown to exercise the perf bot's stage + // breakdown. Remove before merging. Bump the count if the delta is too + // small to cross the threshold. + { + import core.volatile : volatileLoad, volatileStore; + __gshared uint sink; + foreach (i; 0 .. 50) + volatileStore(&sink, volatileLoad(&sink) + i); + } + scope v = new ExpressionSemanticVisitor(sc); e.accept(v); return v.result;