Skip to content

Reduce executor per-round CPU (CDS archive, HttpURLConnection, no redundant class re-parse/check) - #96

Merged
solid-eureka merged 6 commits into
devfrom
perf/executor-round-cost
Nov 2, 2026
Merged

solid-eureka merged 6 commits into
devfrom
perf/executor-round-cost

Conversation

@nils-loose

Copy link
Copy Markdown
Contributor

Reduces the fixed CPU cost of each executor round (one JVM start per explored path) in sv-comp mode.

Changes

  • Transformers return null for classes they do not change. java.lang.instrument treats any non-null return as "transformed", so the JVM re-parsed every class loaded after premain (incl. ~1000 JDK classes otherwise taken from CDS).
  • Trace POST via HttpURLConnection instead of HttpClient. HttpClient.newHttpClient() sets up a default SSLContext, a selector thread and ~200 classes (~0.2 s per round) for a plain-HTTP POST. Same request, same output.
  • AppCDS archive per run (symbolic-explorer/svcomp/ExecutorCDS.py): round 0 writes a class list (-XX:DumpLoadedClassList), before round 3 the explorer runs a static java -Xshare:dump (no agent involved, ~1.5 s CPU), rounds >= 3 start with -XX:SharedArchiveFile. Runs with <= 3 rounds pay nothing; break-even is ~6 rounds.
    • The class path is laid out as <jars of the cp> : <agent jar> : <original cp> because CDS refuses directory entries. This is only done if no moved jar's file name shadows an earlier entry, so every class/resource resolves to the same file; otherwise CDS is off for the run. java.class.path differs as a side effect.
    • Fallbacks: no class list, dump error or timeout -> run continues without archive; an archive the JVM cannot validate is ignored (-Xshare:auto).
    • Disable with --no-executor-cds.
    • The archive (~30 MB) is written to <logdir>/swat-cds-*/ and deleted at the end of a normal run (a killed run leaves it behind). If the log dir is on tmpfs it counts against the memory limit.
    • Dynamic CDS (-XX:ArchiveClassesAtExit) is not usable with a Java agent on JDK 17 (needs a diagnostic flag and crashes at dump).
    • Not yet tested on the SV-COMP image (Ubuntu 26.04, openjdk-17). Measured with Temurin 17.0.16; static -Xshare:dump is a standard JDK 17 feature.
  • CheckClassAdapter only if instrumentation.checkClassAdapter is set. Four transformers always wrote through new CheckClassAdapter(cw, true) (visit checks + data-flow analysis of every method), ignoring the flag that sv-comp.cfg sets to false. Behaviour differs only for invalid instrumented code, which is then no longer rejected while written (it hits the JVM verifier or ASM frame computation instead). New CheckClassAdapterSwitchSpec.
  • Optional phase profile -Dswat.profile=<file> (de.uzl.its.swat.common.Profile; off = one static boolean check).

Measured

CPU per round (user+sys of the JVM, 2 pinned cores, sv-comp.cfg, -Xmx12g, stub explorer):

task dev branch
MinePump/spec1-5_product57, round 25 1.19 s 0.64 s
float_unboundedloop/Cyclic-Preset-Inner-ABC, round 1 0.95 s 0.48 s
jdart-regression/list2, round 100 1.08 s 0.61 s
argv-tasks/ActiveCheck, round 200 (1.5 MB trace) 1.55 s 1.09 s

(Last three rows measured before the CheckClassAdapter commit.) Rounds completed within 300 s CPU on 8 timeout tasks: +31% to +75%.

Verdicts: 64 non-timeout tasks from 15 families: 63/64 identical to dev (the other is a wall-timeout on both). 20 further tasks after the CheckClassAdapter change: 20/20 identical. Explorer unit tests pass; executor test (common.*, instrument.*) and agentTest pass.

Full-campaign numbers follow in a comment.

🤖 Generated with Claude Code

nils-loose and others added 6 commits October 20, 2026 16:19
Records wall clock, thread and process CPU time at the phase boundaries of
one executor run (agent init, premain, execution start/end, trace
serialization, trace transfer, shutdown), class-load/instrumentation
counters and the CPU time per JVM thread. Off unless the property is set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
java.lang.instrument treats any non-null return value as a transformed
class, so returning the unchanged input buffer made the JVM parse every
class loaded after premain from its bytes again, also the JDK classes it
would otherwise map from the CDS archive (about 1000 per executor run).
The class bytes are the same as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Creating a java.net.http.HttpClient initialises an SSL context (reading
the CA certificates), a selector thread and an executor and loads about
200 classes, which cost a large part of a short executor run's CPU time
although the request is plain HTTP to localhost. Same request (POST,
JSON body, Content-Type application/json) and same output line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every round starts a new executor JVM that loads, parses and verifies the
same ~3000 classes (agent jar, JDK classes outside the default CDS
archive). Round 0 now records the loaded classes, the archive is dumped
before round 3 (about 1.6 s CPU, so short runs do not pay for it) and later
rounds map the classes from it: about 0.26 s less CPU per round on
MinePump (0.95 -> 0.69 s).

CDS archives classes from leading jar entries of the class path only, so
the class path gets a prefix of its jars plus the agent jar; this is done
only if no file of a moved jar also exists in an entry before it, so every
name resolves to the same file as before. Transformed classes are still
parsed from the transformed bytes. Any failure falls back to running
without the archive; --no-executor-cds turns it off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ieval

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… is set

Four transformers always wrote the instrumented class through
CheckClassAdapter with data-flow checks, whatever the config said; only
the separate verify step honoured instrumentation.checkClassAdapter. The
checks only catch instrumentation bugs and cost about 0.06 s CPU per
executor run. The writer is now wrapped under the same condition as the
verify step (instrumentation.checkClassAdapter, default false, and
instrumentation.checkClassAdapterClass).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@nils-loose nils-loose added this to the SV-COMP 2027 milestone Oct 21, 2026
@nils-loose

Copy link
Copy Markdown
Contributor Author

Full campaign: 1713 SV-COMP jobs (300 s CPU, no SA, sv-comp.cfg, 15 GB / -Xmx12g), dev 209cc0e and svcomp27/overnight-solver-perf (dev + #96 + #98 + #99) run at the same time, 16 slots x 2 cores each.

dev dev + #96 + #98 + #99
points 2192 2201 (+9)
correct SAFE / VIOLATION 804 / 664 804 / 673
wrong (5 known wrong labels, #63) 5 5
new wrong verdicts 0
timeouts / UNKNOWN 136 / 103 127 / 103
CPU sum 46431 s 40090 s (-14%)

Attribution: the 20 runs that changed were re-run with each change switched off (--no-fp-tactic, --no-executor-cds, without #99).

This PR: no verdict changes (with --no-executor-cds the 20 changed runs give the same verdicts as with it). CPU only: on runs with the same decided verdict on both sides, e.g. jpf-regression -15%, MinePump -15%, objects -42%, jayhorn-recursive -26%, float_unboundedloop -30% (these families have no FP/char-code queries, so the gain is from this PR); all families -19% (combined with #98/#99).

AppCDS: 128 runs left a swat-cds-* directory (~23 MB each) behind — exactly the 128 runs killed at the CPU/wall limit; normally finished runs cleaned up. Still untested on the Ubuntu 26.04 / openjdk-17 image.

🤖 Generated with Claude Code

@solid-eureka
solid-eureka merged commit c1f1456 into dev Nov 2, 2026
10 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants