Repository navigation
perf(ownership): compute the ownership summaries with fewer solves and rounds - #187
Merged
Merged
Conversation
This was referenced Oct 7, 2026
SizzleUnrlsd
force-pushed
the
184-ownership-summary-cost
branch
from
October 7, 2026 21:52
28b5e40 to
8f74e11
Compare
This was referenced Oct 7, 2026
…tive model When jobs are automatic, the default, files compile in parallel and each one changes the working directory of the whole process to its compile command's directory (#184). The changes interleave: a file can compile from another file's directory, and the analyzer can end in one of them, where a relative resource model or cache directory no longer resolves. check_resource_model_across_compile_directories generates 8 files, each in a directory of its own with the one header it includes through a relative -I, and a resource model, given by a relative path, that alone defines the leak every file has. It requires: - with automatic jobs, --jobs=1 and --jobs=4, three runs each with empty caches: the model loads, the 8 leaks and no other are reported, and the compile IR and resource caches are under the start directory; - the relative model and cache paths anchored to the start directory (--print-effective-config); - no compile with a changed working directory, under automatic jobs, for compile commands that need none (--timing). On main, automatic jobs fail every run ('unit6.h' file not found, no output), --jobs=4 writes caches under the compile directories, the paths stay relative, and automatic jobs change the directory for every file. Refs #184
…y change A compile that needs its compile command's directory changes the working directory of the whole process (#184). Three things let that leak into the rest of the run: - InputPipeline chose its path with config.jobs > 1, but jobs is 0 when automatic, the default: parallel loads then changed the directory for every file. It now uses resolveConfiguredJobs, moved out of AnalyzerApp into StackUsageAnalyzer for both, so parallel runs try a compile from the current directory first, as with an explicit job count. - The change was made before taking the compile lock, and the separate lock of the fallback covered fallbacks only. Two threads could save and restore the directory in an interleaved order, compile in a directory not their own, and leave the process in one. The compile lock is now taken before the change and held until it is undone; the fallback lock goes. - The resource, escape and buffer models, the compile IR and resource summary caches, the --dump-ir output and the compile database were opened through paths relative to whatever the directory was at that time. They are now anchored to the directory the analyzer starts from, once the arguments are parsed. A compile from its command's directory still changes the directory of the whole process while it runs; removing that needs compilerlib to compile against a working directory of its own. Refs #184
…mand's Review of #184: a compile command's relative -I resolves against its directory, but the analyzer first compiled from the current directory and only used the command's directory when that compile failed. When both directories hold the header, the start directory's one was used, with no error: on main with --jobs>1, and with automatic jobs since the fix for the working-directory race. check_resource_model_across_compile_directories now puts, in the start directory, a header of the same name for every file, one that drops the leak. Each mode must still report the 8 leaks. On 3b2260e, automatic jobs and --jobs=4 report none; --jobs=1 reports all 8. The check no longer requires automatic jobs to compile commands with absolute paths without changing the working directory: that was the behavior that picks the wrong header. Refs #184
A compile command whose directory differs from the current one was first compiled from the current directory, and from its own only when that failed. Relative paths then resolved against the wrong directory without any error when both held the file: another header, another IR. This "optimistic fast path" (2289afb) predates the lock that serializes every compile (967be74), so it no longer ran anything in parallel. A command with a directory is now always compiled there, under the compile lock, whatever the job count. The retry and its log suffix go, and resolveConfiguredJobs returns to AnalyzerApp, its only user again. Refs #184
Review of #184: each analysis loads its model itself, logs a failure to stderr and goes on without it. A resource, escape or buffer model given on the command line but missing ends with code 0 and no finding, and run_code_analysis.py reports "CI gate passed". check_unreadable_model_fails gives each of the three options a missing file and a directory: the analyzer must exit with a nonzero code naming the path. run_code_analysis.py given a missing resource model must fail. On 274c1c1 all six runs exit with 0 and the CI script passes. A model that can be read but is malformed keeps its current handling (check_escape_model_rejects_unsupported_brackets); it is a separate question. Refs #184
The resource, escape and buffer models are read by the analyses that use them, which log a failure and run without the model. A missing model then passed for a run that found nothing, and run_code_analysis.py for a passing CI gate. Before any input is loaded, every model given on the command line or in the config file must now be a readable file; otherwise the run stops with "Cannot read <kind> model file: <path>" and exit code 1, which the CI script already turns into a failure. This check covers missing and unreadable files only. A model that can be read but holds malformed rules is still reported on stderr and skipped by the analysis: a separate question, noted in the README. Refs #184
SizzleUnrlsd
force-pushed
the
184-ownership-summary-cost
branch
from
October 8, 2026 02:21
8f74e11 to
4f5d3a8
Compare
This was referenced Oct 8, 2026
…irectories Review of #187: a compile command with a directory of its own is now always compiled there, and the change of directory holds for the whole process. Meanwhile, another thread resolves its own relative input path against that directory, to find its compile command and to compile it: with 24 files given by relative paths, --jobs=4 fails with "No compile command found for: src/unit03.c", where --jobs=1 and main succeed. check_resource_model_across_compile_directories now runs every mode with the files given by absolute paths, then by paths relative to the start directory, each run with empty caches of its own. Each must report the 8 leaks and name each file as it was given (#124). On e2e9f87, 5 of the 6 parallel runs with relative paths fail; --jobs=1 and absolute paths pass. Refs #184
…tarted in A compile command with a directory of its own is compiled from there, and the change of directory holds for the whole process while it lasts. loadModuleForAnalysis resolved a relative input against the current directory: to find its compile command, to compile it, for its compile IR cache key and to read it. Another thread could then resolve it from a compile command's directory, and with --jobs=4 an input given as src/unit03.c had no compile command. The directory the analyzer starts from is now recorded with the other anchored paths, as AnalysisConfig::inputBaseDir, before any input is loaded. Every access to an input goes through its path resolved from there. The path given is kept for messages and results (#124), and a file without a compile command is still compiled under the name given: no other thread changes the directory while a compile holds the lock. Refs #184
…compiled again Review of #187: the compile IR cache keeps the IR that the previous pipeline built from the current directory, with a header of the same name as the compile command's own, under schema compile-ir-cache-v2. A run that now compiles from the command's directory reused it: 7 where a run with an empty cache gives 42. check_compile_ir_cache_rebuilds_previous_schema rewrites the entry of the current key as the previous pipeline left it: the previous schema, and the IR built with the start directory's header, with the sources and dependencies left valid. The next run must compile again, give 42 and rewrite the entry, and the run after it must reuse that entry. An entry stored under a previous key is never looked up again; the schema in the key covers it, not this check. On c5911fb the stale entry is reused: 7. Refs #184
A compile command with a directory of its own is now always compiled from there, so an unchanged command can give other IR than before: the IR of a file whose header of the same name also sits in the start directory. The compile IR cache kept the previous IR under the same key and accepted it, and the fix had no effect on a cache filled before it. The schema becomes compile-ir-cache-v3, in the cache key and in the metadata of each entry: previous entries are no longer looked up, and an entry whose metadata names the previous schema is compiled again. Refs #184
…e per parameter computeSummary solves the whole function 4 * (parameters + pointee paths) + 1 times (#184): 101 solves for a function with 25, which makes the self-analysis run for minutes once the resource model loads. - A reference, written with the public solve(), builds the transformers as computeSummary does today: one solve per parameter and entry state, the other parameters Owned, and one with every parameter Owned for the fresh resources. computeSummary must give the same transformers, and be incomplete exactly when the reference is, with aliases, pointee paths, a loop, exceptional exits, and at the smallest budget the reference converges with and just below it. - Fresh resources beside several parameters: a returned one and one handed through a pointee, guaranteed then conditional. - Work: SummaryWork counts the solves and block visits of a summary. Six parameters and pointees take four solves and at most four times the block visits of one solve; a function without parameters, one. Refs #184
SummaryWork, an optional out-parameter of computeSummary, counts the solves a summary takes and the blocks their worklists process; solve() reports its own block visits. They only observe: no summary depends on them. With the summary computed as before, the test of a function with six parameters and pointees reports 25 solves and 100 block visits, where one solve visits 4 blocks. Refs #184
…y state computeSummary solved the whole function once per parameter and entry state, the other parameters Owned, and once more with all of them Owned for the fresh resources: 4 * (parameters + pointee paths) + 1 solves, 101 for a function with 25 (#184). It now solves once per entry state, with every parameter in that state. Each parameter has a resource of its own; an event acts on a resource from that resource's own state, what a location holds never depends on any state, and paths join resource by resource. Each parameter therefore ends as in a solve of its own with the others Owned. The solve with every parameter Owned is the one the fresh resources were read off; without parameters it remains the only one. Four solves instead of 4 * P + 1; the test of six parameters and pointees goes from 25 solves and 100 block visits to 4 and 16. Refs #184
…es changed computeOwnershipSummaries recomputes every function of the module each round, up to 16, although a round may change a single summary (#184): the self-analysis computes 88 458 summaries for 53 files. A reference written in the test runs that fixed point: every function recomputed in index order each round until a round changes none, every summary incomplete past the budget. computeSummaryFixpoint must give the same summaries with no more computations: independent functions, a chain against the index order, recursion, mutual recursion, a change of the incomplete status alone, a consulted summary that changes later in the same round while the computations that consulted it changed nothing, and an exhausted budget, which takes exactly 16 rounds. Independent functions are computed once each, and the chain fewer times than every function each round. SummaryFixpointWork counts the computations, the reused summaries and the rounds; sameSummary compares summaries, incompleteness included. Refs #184
…ryFixpoint The fixed point of computeOwnershipSummaries moves into the ownership engine, as computeSummaryFixpoint, which knows nothing of LLVM. It runs the same rounds in the same order, starts from the same empty summaries, stops after the first round that changes nothing, and past 16 rounds marks every summary incomplete. The summaries of the module's functions are now read through consult; those of other modules, which do not change here, are still read directly. sameSummary, the comparison of summaries, moves into the engine with the same fields; ownershipSummaryEquals keeps its interface and calls it. Every function is still recomputed each round: the work tests report 10 computations for 5 independent functions and 42 for the chain. Refs #184
computeSummaryFixpoint recomputed every function each round. A summary is now kept when none of the summaries its last computation consulted has changed since: computing it again would give the same summary, as the computation depends on nothing else that changes. Each summary has a version raised when it changes, its incompleteness included; each computation records the version of every summary it consults, its own when recursive. The rounds, their order, the stop after a round that changes nothing and the incomplete marking after 16 rounds are unchanged. The work tests go from 10 to 5 computations for 5 independent functions and from 42 to 21 for the chain. Refs #184
SizzleUnrlsd
force-pushed
the
184-ownership-summary-cost
branch
from
October 8, 2026 16:30
4f5d3a8 to
ecf7f07
Compare
SizzleUnrlsd
marked this pull request as ready for review
October 8, 2026 17:12
This was referenced Oct 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part 2 of #184: the cost of the ownership summaries. It builds on part 1 (#186), which makes the resource model load in every run, compiles every command in its own directory, resolves relative inputs from the start directory, moves the compile IR cache to schema v3, and stops when a requested model cannot be read. CI only runs for pull requests into
main, so this PR targetsmainand also carries #186's ten commits. It is the PR planned for delivery: merging it also delivers #186.Problem
With the resource model loaded, the self-analysis spends most of its time in
computeOwnershipSummaries:computeSummarysolved each function once per parameter (and pointee path) and per entry state;One 207-function module cost about 90 s per round.
Changes
computeSummary: one solve per entry state, with every parameter in that state. This relies on resources being independent of each other. Fresh resources come from the all-Owned solve; a function without parameters takes a single solve. On the work-test function: 4 solves and 16 block visits, against 25 and 100 before.computeSummaryFixpoint: keeps a summary while every summary it consulted is unchanged. The order, the 16-round budget and the global incomplete marking when the budget runs out are unchanged.consult.sameSummarycompares the same fields as the previous comparison.SummaryWork,SummaryFixpointWorkandOwnershipResult::blockVisitsonly count work.Validation
ownership_engine_unit_tests):fixtures, 419 units alone, default and z3 passes: 838 runs, 1 275 diagnostics on each side, 0 differences;
self-analysis, the 4 CI chunks with the CI arguments: 70 diagnostics on each side (17, 18, 26, 9 per chunk), 0 differences;
Lua 5.4.8 together: the run with the previous algorithm never finished. It was still going after 8 h 54 min, and stopped with the session that launched it. In its place, A+B was compared with A (397b28b) on the same 34 files, with the same options and the model loaded:
Together with A's summary-level equivalence to the previous algorithm on Lua (81 483 summaries, 0 mismatches), this closes the chain.
mainc1e60b8 (with fix(analysis): resolve calls through constructor and destructor aliases #190), head ecf7f07 (also checked on 4f5d3a8 before fix(input): load models and caches the same way whatever the job count #186's last two fixes):run_test.pypasses 2383/2383, both without and with CI's compile IR cache option, and both unit-test binaries pass (macOS arm64);main: 423 units for the host, and forx86_64-unknown-linux-gnuthe 274 that compile without a Linux sysroot.CI time of the Self-analysis step (Linux, 15-minute limit)
workflow_dispatchon the temporary branch184-cold-ci-measureRUNNER_TEMP, absent before the stepThe measurement run used the same arguments as this step, plus
--analyzer-arg=--resource-summary-cache-dir=$RUNNER_TEMP/resource-summary-cache-cold-<run>:The compile IR cache is not used by this step. Before the rebase, both runs of this PR found 0 errors, 9 warnings and 63 infos; after it, 0 errors, 8 warnings and 63 infos. The warning that goes is alert 251, which #190 removes.
Code scanning
ConstParameterNotModified.PointeroncomputeSummary'sSummaryWork* work, which the lambdasolveCountedmodifies (False positive: ConstParameterNotModified.Pointer on a pointee modified in a lambda #188);UninitializedLocalReadonParamTransformer normal{}andexceptional{}, value-initialized, in the test's reference loop (False positive: UninitializedLocalRead on a value-initialized local #189).UninitializedLocalReadfalse positive on the previouscomputeSummaryloop. That loop moved into the unit test as the reference, which moved the alerts.Closes #184