revert: JavaScript one-start build did not deliver in the push path (M2 11.6)

Reverts f04a839 on its own measurement. The seven-class profile rerun recorded
joern_parse_census_incomplete on 12 transactions: the faster build was rejected by
its own census gate and the frontend plus overlay pass ran as well, so those
transactions paid two engine starts instead of one. Every workload kept the same
p50 and the verdict stayed NO-GO.

The cause is an asymmetric gate. The frontend path is not census-gated at build
time, so whatever it produces is accepted, while the joern-parse path was gated and
the same shortfall rejected it. Holding the new path to a standard the old one
never has to meet guarantees the fallback fires. The probe tree and the
materialized push snapshot also differ, so equivalence on the checkout did not
transfer to the real path.

What still stands is the underlying timing fact, measured under the production
retention environment on identical graphs, same 44 files, 579 methods, file names,
overlays, Gruntfile and eighteen test files: 11.0 s against 21.0 s. JavaScript does
pay two engine starts where every other language pays one. A future attempt needs a
gate both paths are held to, or a direct comparison against what the frontend would
produce, and must be measured in the push path rather than on a checkout.

The profile also names the remaining cost plainly: a changed-code transaction
spends its budget before any query runs, head build 25.2 s of a 30 s deadline with
query time 0.0 s. Building one new graph per push is the cost, not querying it.

1,401 tests passed, nine skipped. Rollout stays NO-GO.

Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
This commit is contained in:
Marius Ciepluch
2026-09-17 09:04:44 +02:00
co-authored by Claude Opus 5
parent f04a839181
commit ed0f12b593
5 changed files with 155 additions and 51 deletions
@@ -146,5 +146,9 @@
"limits": [
"One pinned JavaScript tree. PHP keeps the frontend path, which it needs for exclusions and retries.",
"A build saving is not the latency gate: the profile rerun decides that."
]
],
"outcome": "REVERTED",
"why_reverted": "Measured in the real push path the change did not deliver and cost an extra engine start. The seven-class profile rerun recorded joern_parse_census_incomplete on 12 transactions, so the faster path was rejected by its own census gate and the frontend plus overlay pass ran as well. Every workload stayed at the same p50 and the verdict stayed NO-GO.",
"root_cause_of_the_failed_gate": "The gate is asymmetric. The frontend path is not census-gated at build time, so whatever it produces is accepted; the joern-parse path was gated, so the same shortfall rejected it. Holding the new path to a standard the old path never has to meet guarantees the fallback fires. The probe tree and the materialized push snapshot also differ, so equivalence on the checkout did not transfer.",
"what_still_stands": "The timing difference is real and unexplained by anything else: 11.0 s against 21.0 s for identical graphs under the production retention environment, same 44 files, 579 methods, file names, overlays, Gruntfile and eighteen test files. JavaScript genuinely pays two engine starts where other languages pay one. A future attempt needs a gate both paths are held to, or a comparison against what the frontend would produce."
}
@@ -0,0 +1,136 @@
{
"workloads": {
"identical-tip": {
"population": 3,
"observed": 3,
"missing": 0,
"latency_samples": 3,
"p50_seconds": 23.848443167982623,
"p95_seconds": 23.990965132950805,
"timeouts": 0,
"completed_targets": 0,
"expected_targets": 3,
"complete_target_cases": 0,
"completion": 0.0,
"complete_coverage_cases": 0,
"cancellation_overrun_seconds": 0,
"pass": false
},
"function-edit": {
"population": 3,
"observed": 3,
"missing": 0,
"latency_samples": 3,
"p50_seconds": 30.669465193990618,
"p95_seconds": 30.692176087992266,
"timeouts": 3,
"completed_targets": 0,
"expected_targets": 3,
"complete_target_cases": 0,
"completion": 0.0,
"complete_coverage_cases": 0,
"cancellation_overrun_seconds": 0.6921760879922658,
"pass": false
},
"dependency-edit": {
"population": 3,
"observed": 3,
"missing": 0,
"latency_samples": 3,
"p50_seconds": 30.696090501034632,
"p95_seconds": 30.741624664980918,
"timeouts": 3,
"completed_targets": 0,
"expected_targets": 3,
"complete_target_cases": 0,
"completion": 0.0,
"complete_coverage_cases": 0,
"cancellation_overrun_seconds": 0.7416246649809182,
"pass": false
},
"configuration-edit": {
"population": 3,
"observed": 3,
"missing": 0,
"latency_samples": 3,
"p50_seconds": 30.67598664702382,
"p95_seconds": 30.68519904196728,
"timeouts": 3,
"completed_targets": 0,
"expected_targets": 3,
"complete_target_cases": 0,
"completion": 0.0,
"complete_coverage_cases": 0,
"cancellation_overrun_seconds": 0.6851990419672802,
"pass": false
},
"cold": {
"population": 3,
"observed": 3,
"missing": 0,
"latency_samples": 3,
"p50_seconds": 30.65046051493846,
"p95_seconds": 30.686437385971658,
"timeouts": 3,
"completed_targets": 0,
"expected_targets": 3,
"complete_target_cases": 0,
"completion": 0.0,
"complete_coverage_cases": 0,
"cancellation_overrun_seconds": 0.6864373859716579,
"pass": false
},
"growth": {
"population": 3,
"observed": 3,
"missing": 0,
"latency_samples": 3,
"p50_seconds": 30.951075186021626,
"p95_seconds": 30.9925197820412,
"timeouts": 3,
"completed_targets": 0,
"expected_targets": 3,
"complete_target_cases": 0,
"completion": 0.0,
"complete_coverage_cases": 0,
"cancellation_overrun_seconds": 0.9925197820411995,
"pass": false
},
"multi-ref": {
"population": 3,
"observed": 3,
"missing": 0,
"latency_samples": 3,
"p50_seconds": 30.678902502986602,
"p95_seconds": 30.69903396104928,
"timeouts": 3,
"completed_targets": 0,
"expected_targets": 6,
"complete_target_cases": 0,
"completion": 0.0,
"complete_coverage_cases": 0,
"cancellation_overrun_seconds": 0.6990339610492811,
"pass": false
}
},
"runtime_verdict": "NO-GO",
"rollout": "NO-GO",
"qualification_limit": "Development NodeGoat, n=3 per workload; independent quality and multi-project performance not established.",
"missing_workloads": [],
"profile_sha256": "26dee90b715f6944651b5022bda573e1bdbeb6a9ea1249c0191c44506fb84d3b",
"priming_completed": false,
"experiment": "session-runtime-profile-v2",
"milestone": "M2 task 11.6",
"bundle": {
"path": "2026-09-17-session-runtime-profile-after-build-change.json.gz",
"sha256": "c83474fbf051156ae1d1f6565001e25d724f2588146dd5114cb00da686e3c63c",
"files": 108
},
"change_under_test": "JavaScript built by joern-parse in one engine start, gated on the census; since reverted.",
"diagnostics": {
"joern_parse_census_incomplete": 12
},
"finding": "The change did not deliver in the real push path. Its own census gate rejected the faster build on 12 transactions, so the frontend and overlay pass ran as well and the transaction paid two engine starts instead of one. Every workload kept the same p50 and the verdict stayed NO-GO, so it was reverted.",
"cause": "An asymmetric gate. The frontend path is not census-gated at build time, so whatever it produces is accepted, while the joern-parse path was gated and the same shortfall rejected it.",
"remaining_cost": "A changed-code transaction spends its whole budget before any query runs: head build 25.2 s of a 30 s deadline with query time 0.0 s. Building one new graph per push is the cost, not querying it."
}
+8 -30
View File
@@ -552,36 +552,14 @@ class JoernBackend:
execution_diagnostics=lambda: tuple(self._diagnostics),
)
if language.lower() in {"javascript", "typescript"}:
# M2 11.6: `joern-parse` applies the overlays itself, so it is ONE engine start where the
# frontend plus a separate overlay pass is two. On the pinned Node tree that is 11.0 s
# against 21.0 s, and JavaScript was the only language paying twice.
#
# The declared frontend stays, because it is what the versioned retention adaptation and
# the explicit exclusion policy run through, and a build nine seconds faster that quietly
# drops a file is worse rather than better. Measured under the production retention
# environment both paths hold the same 44 files and 579 methods, the same file names, the
# same overlays, the Gruntfile and all eighteen first-party test files. That is why this
# is allowed at all, and the census still decides per build: an incomplete graph is
# rebuilt through the frontend rather than trusted.
parsed_unparsed: tuple[str, ...] | None = None
if not exclude and parse is not None:
command = [parse, self._heap_flag(), str(root), "--output", str(cpg_path)]
completed = self._run(command, timeout=self.build_timeout, cwd=scratch)
if completed is not None and completed.returncode == 0 and cpg_path.is_file():
if self._graph_is_complete(cpg_path, root, language):
parsed_unparsed = _unparsed_files(completed)
else:
self._note("joern_parse_census_incomplete")
logger.info("joern-parse produced an incomplete graph for %s; rebuilding through the frontend", root)
if parsed_unparsed is not None:
unparsed = parsed_unparsed
else:
direct = self._build_with_frontend(root, cpg_path, scratch, language, exclude)
if direct is None:
dispose()
return None
self._apply_overlays(cpg_path, scratch)
unparsed = direct
# Use the declared frontend rather than language autodetection. The versioned
# retention adaptation consumes the explicit include_tests subprocess policy.
direct = self._build_with_frontend(root, cpg_path, scratch, language, exclude)
if direct is None:
dispose()
return None
self._apply_overlays(cpg_path, scratch)
unparsed = direct
else:
command = [parse or "joern-parse", self._heap_flag(), str(root), "--output", str(cpg_path)]
completed = self._run(command, timeout=self.build_timeout, cwd=scratch)
+6 -20
View File
@@ -1135,41 +1135,27 @@ def test_the_interpreter_must_read_the_frontend_parser_too(tmp_path: Path, monke
assert "php-parser.php" in backend.last_failure and "mounts the repository but not this install" in backend.last_failure
def test_javascript_falls_back_to_the_declared_frontend_when_the_census_is_short(tmp_path, monkeypatch):
"""M2 11.6: one engine start is allowed only while the graph still holds every declared file.
The retention adaptation runs through the declared frontend, and it is what took the Node census
from 43 files to 44. So an incomplete graph from the faster path is rebuilt rather than trusted.
"""
def test_javascript_partition_uses_declared_frontend_not_legacy_autodetection(tmp_path, monkeypatch):
from openultrasast.cpg.backend import JoernBackend
(tmp_path / "tests").mkdir()
(tmp_path / "tests/probe.js").write_bytes(b"function first_party_test(x) { return x; }")
(tmp_path / "app.js").write_bytes(b"function handler(x) { return x; }")
commands = []
def runner(command, **kwargs):
if _is_overlay(command):
return _overlaid(command, kwargs.get("cwd"))
commands.append(Path(command[0]).name)
if command[0].endswith("census.sc") or "census" in " ".join(command):
# One file short of the two on disk: the faster path must not be trusted.
body = '{"files":"1","methods":"1","overlays":"base,dataflowOss","maxHeapMB":"2048"}'
payload = "---OUSAST-CPG-BEGIN---\n" + body + "\n---OUSAST-CPG-END---"
return subprocess.CompletedProcess(command, 0, payload, "")
target = command[command.index("-o") + 1] if "-o" in command else command[command.index("--output") + 1]
Path(target).write_bytes(b"graph")
commands.append(command)
assert Path(command[0]).name == "jssrc2cpg"
Path(command[command.index("-o") + 1]).write_bytes(b"graph")
return subprocess.CompletedProcess(command, 0, "", "")
monkeypatch.setattr("shutil.which", lambda name: f"/opt/joern/{name}")
monkeypatch.setenv("OPENULTRASAST_JOERN_PROBE", "on")
backend = JoernBackend(runner=runner)
result = backend.build(tmp_path, language="javascript")
result = JoernBackend(runner=runner).build(tmp_path, language="javascript")
try:
assert result is not None
assert "joern-parse" in commands, "the one-start path should be tried first"
assert "jssrc2cpg" in commands, "a short census must rebuild through the declared frontend"
assert "joern_parse_census_incomplete" in backend._diagnostics
assert len(commands) == 1
finally:
if result is not None:
result.cleanup()