perf(model): parse, index and validate a load's files on a pool of workers - #312
Merged
Merged
Conversation
Files loaded from the command line or by %load were joined into the transcript document, so a root-level import in one file served the others and two files declaring one root package were reported as duplicates. Each loaded file is now a workspace document under its own name, indexed with the others and analyzed on its own, as the editor and the corpus gates analyze it; the typed transcript stays one joined document. A differential test runs every multi-file directory of the fixtures and the OMG corpora through the command line and a workspace and asserts the same diagnostics. Co-Authored-By: jason.han <hanhuijun@gmail.com>
…pt alone Co-Authored-By: jason.han <hanhuijun@gmail.com>
…ing skill Co-Authored-By: jason.han <hanhuijun@gmail.com>
…kers A workspace opens a batch of documents in one step: the files are parsed and their scope trees built on workers, added to the one index in order, and the wildcard imports expanded once for the batch. Their diagnostics are computed on workers too, each with a context of its own over the index; before the pool starts, the metadata body scopes of the batch are linked to their owners, which resolving would otherwise write into the shared tree on first use. The results come back in the order asked, the same at any worker count. The document's own scope tree is the one the index holds, so a document is built once rather than twice. The REPL loads files through the batch; -workers and OPENSYSML_WORKERS set the count, one per CPU by default. The stress generator gains -split-planes. Co-Authored-By: jason.han <hanhuijun@gmail.com>
…al, and benchmark it -workers and OPENSYSML_WORKERS are checked from the command line over a model of several files, and BenchmarkValidateSplit loads the network split by plane on one worker and on one per CPU. Co-Authored-By: jason.han <hanhuijun@gmail.com>
Co-Authored-By: jason.han <hanhuijun@gmail.com>
…, and the gather it parallelizes Records the 200- and 1 600-satellite splits at one, two, four and eight workers beside the single file, the CPU and heap profiles that put the split's cost in the three workspace-wide audits, the allocation sites the pool does not help, and a benchmark of the per-document analysis alone. Co-Authored-By: jason.han <hanhuijun@gmail.com>
Co-Authored-By: jason.han <hanhuijun@gmail.com>
…er tools use Co-Authored-By: jason.han <hanhuijun@gmail.com>
Contributor
Author
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
6 tasks
… load OPENSYSML_WORKERS now answers to its legacy SYSML_ name like the other variables, and -query, -render, -render-all and -compile resolve the run bounds before loading, as the other loading modes do. Co-Authored-By: jason.han <hanhuijun@gmail.com>
6 tasks
Co-Authored-By: jason.han <hanhuijun@gmail.com>
…lidation Co-Authored-By: jason.han <hanhuijun@gmail.com>
A loaded file is analyzed as a document of its own, so an error in it gates that file's deeper checks only. The blocker note on a clean prompt submission now skips diagnostics from loaded files, and a load's from the transcript. Co-Authored-By: jason.han <hanhuijun@gmail.com>
A load shares no document with the rest of the buffer, so nothing blocks it and it neither names nor forgets the error the transcript has already been told of. Co-Authored-By: jason.han <hanhuijun@gmail.com>
… interval A load still names no blocker, but when it leaves the transcript unblocked the recorded note is cleared, so the error is named again should a reload bring it back. Co-Authored-By: jason.han <hanhuijun@gmail.com>
OpenAll parses outside the lock, so a document another caller opened, edited, closed or removed meanwhile was overwritten at commit. The batch now records what each name held as it started and installs only where that still stands. Co-Authored-By: jason.han <hanhuijun@gmail.com>
…ocuments Co-Authored-By: jason.han <hanhuijun@gmail.com> # Conflicts: # README.md
…lidation Co-Authored-By: jason.han <hanhuijun@gmail.com> # Conflicts: # README.md # docs/project/spec-compliance.md
… document is left A file reloaded with its enclosure left open is masked and its workspace document removed; with no scoped document left, symbolIndex returned before taking the file's previous declarations back out of the session index, so a qualified lookup kept answering with what the session no longer held. The empty-document path now drops every indexed document, as a reset does, and keeps the standard library. Co-Authored-By: jason.han <hanhuijun@gmail.com>
…lidation Co-Authored-By: jason.han <hanhuijun@gmail.com> # Conflicts: # README.md # docs/project/spec-compliance.md
…ocuments Co-Authored-By: jason.han <hanhuijun@gmail.com> # Conflicts: # README.md # docs/project/spec-compliance.md
…lidation Co-Authored-By: jason.han <hanhuijun@gmail.com> # Conflicts: # README.md # docs/project/spec-compliance.md # internal/core/model/workspace.go
…del does A batch's fresh models had no source lookup, so a filter on Comment::body, Documentation::body or TextualRepresentation::body was unevaluable in DiagnosticsAll and kept every candidate an editor's model hides. The batch now carries the workspace's read-only source lookup to the preparatory linker and every worker's model. Co-Authored-By: jason.han <hanhuijun@gmail.com>
…ocuments Co-Authored-By: jason.han <hanhuijun@gmail.com> # Conflicts: # README.md # docs/project/spec-compliance.md
…lidation Co-Authored-By: jason.han <hanhuijun@gmail.com> # Conflicts: # README.md # docs/project/spec-compliance.md
…ocuments Co-Authored-By: jason.han <hanhuijun@gmail.com> # Conflicts: # README.md # docs/project/spec-compliance.md
…lidation Co-Authored-By: jason.han <hanhuijun@gmail.com> # Conflicts: # README.md # docs/project/spec-compliance.md
…lidation Co-Authored-By: jason.han <hanhuijun@gmail.com> # Conflicts: # internal/core/edit/edit.go # internal/core/model/workspace.go
This was referenced Sep 17, 2026
docs(roadmap): record E1, the fUML referee and the state-executor findings as landed on develop
#355
Merged
Co-Authored-By: jason.han <hanhuijun@gmail.com>
…h-validation Moves the batch pipeline onto the restructured tree, folds the worker count into the existing -jobs setting, hands the workspace's settled gathers to the batch workers, keeps one Split implementation in tests/stressmodel, and refreshes the measured figures. Co-Authored-By: jason.han <hanhuijun@gmail.com>
…nostics cache An edit queues the regathers it takes for the next read (settleGathersLocked); the editor path settles them before it serves a cached verdict, the batch path did not, so DiagnosticsAll after an edit could answer with an entry the settle would have dropped. The batch now settles first, pinned by TestBatchSettlesPendingGathersBeforeServingTheCache. The performance internals also describe what the batch's gather is: one of its own, gathered once per batch, not the workspace's. Co-Authored-By: jason.han <hanhuijun@gmail.com>
…parsed and validated at once Co-Authored-By: jason.han <hanhuijun@gmail.com>
…ad order Co-Authored-By: jason.han <hanhuijun@gmail.com>
…diagnostic invalidation Co-Authored-By: jason.han <hanhuijun@gmail.com>
Contributor
Author
The typed transcript is the workspace document <repl>, so a file of that name would share its key and each would overwrite the other. Every path loader (LoadPaths, LoadFile, LoadFilesSummary, %load) now reads through one helper that refuses such a file with a *ReservedNameError before anything is submitted; the CLI exits as it does for any read failure. Co-Authored-By: jason.han <hanhuijun@gmail.com>
… merge develop's sessionSourceFile still distinguished the joined KerML buffer; with each loaded file a document of its own only the transcript's spans need the snippet lookup. Co-Authored-By: jason.han <hanhuijun@gmail.com>
…lidation Co-Authored-By: jason.han <hanhuijun@gmail.com> # Conflicts: # cmd/sysml/load_test.go
SubmitFiles takes SourceFile values without the path loaders, so a file named <repl> reached openDocuments under the transcript's workspace key and its findings and declarations were read twice. The submission is now refused whole before anything is accepted; Result.Refused carries the *ReservedNameError and the rendering is that one error line. Co-Authored-By: jason.han <hanhuijun@gmail.com>
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.
What and why
sysml -validate a.sysml b.sysml …,-satisfy,-checkand%loadnow open the files they are given as one batch and validate them on a pool of workers, sized by the existing-jobssetting. This is the parallel batch pipeline ofdocs/project/large-model-scaling-design.md§5, built on #309 (one workspace document per loaded file), which this branch contains; merge #309 first, then this. Until #309 lands, the diff shown here includes its commits — the changes of this PR alone aregit diff feature/per-file-documents...perf/parallel-batch-validation(31 files).The pipeline, in
internal/workspace/model/batch.go:symbols.Index.AddBuiltDocumenttakes a scope tree the caller built withsymbols.Build, so the batch builds its trees on the pool and the single writer only installs them. Wildcard imports are expanded once per batch rather than once per file added, which retires the quadratic per-file reindex cost the stress-test record noted.passes.AnalyzeInBatchin akit.Contextof its own; nothing takes a lock inside the resolver or the model. The one place resolution wrote to the shared scope tree — linking a metadata annotation body's owner (Scope.SetOwner) on first use — is done for all documents of the batch before the pool starts (resolve.(*Resolver).LinkMetadataBodiesviapasses.PrepareBatch, with the model-attached resolver analysis itself uses), so the workers only read it. A metadata body scope records the declaration its annotation is written on (Scope.Annotated()), set by the builder, so the link is computable outside a resolution.passes.Batch.Sourcecarries the workspace's read-only source lookup to the preparatory linker and each worker model, so an import filtered onComment::bodyis evaluated as the editor path evaluates it.passes.Batch.Gatherscarries onepasses.Gathersto every context of the batch. The first context that runs a workspace-wide audit (OOSEMMethodPass,IdentityMetadataPass,MOSAPass) gathers every document's facts into it under its lock; every context afterwards reads the same union. The three passes are untouched — they already read throughContext.Gathers();Context.InBatchsets the context's gathers to the batch's. The gathers are the batch's, not the workspace's persistent ones, because a private resolver records no dependencies and facts gathered by one could not be invalidated per dependency.DiagnosticsAllrecords the names it analyzed inWorkspace.batched;invalidateLockeddeletes thosediagCacheentries before asking the resolver what a change invalidates, andinvalidateAllLockedclears the set. Since the workspace settles the regathers an edit queues on the next read (settleGathersLocked) rather than inside the edit,DiagnosticsAllsettles them first, so a verdictDiagnosticscached is not served once an edit to another document has undone it (TestBatchSettlesPendingGathersBeforeServingTheCache). ADiagnosticscall after an edit re-analyzes on the persistent model, with dependencies, as before;resolve_race_test.goandlazy_regather_test.gopass with the pool.OpenAllparses outside the lock, so it records each input name's change count (Workspace.changes, bumped on every install and removal) before parsing and installs a document only where the count is still the one it reserved; a name opened, updated, closed or removed meanwhile — including one opened and removed again, absent both before and after — keeps its newer state (TestOpenAllKeepsAChangeMadeWhileItParsed).TestParallelBatchValidationMatchesSerialininternal/workspace/modelruns every fixture directory,examples/and the four OMG corpus roots at one job and at GOMAXPROCS and asserts identical diagnostics (content and order), and identical to opening the files one by one.-jobssizes the pool. No new flag:-jobs N,OPENSYSML_JOBSand%jobs— the setting that already bounds how many runs of one check go concurrently (analysis.DefaultJobs,ParseJobs,JobsFromEnv) — also set how many files of one load are parsed and validated at once.Session.SetJobssizes the workspace's pool (Workspace.SetWorkers, the internal pool size); a value below one is rejected at startup before anything loads, in every mode that loads files (TestJobsGovernLoads). Documented insysml -help, the environment listing,docs/reference/{cli,environment,repl-commands}.mdand the regenerated man page (make man).Documentstays immutable and analysis state lives in each context; nothing in the pipeline holds a tree past its analysis except the workspace's owndocsmap, so releasing trees once interface records exist (§4) is a change to that map alone. Nothing is dropped in this PR.tools/cmd/stress-model -split-planes <dir>writes the oneSatelliteNetwork.Split()oftests/stressmodel/split.goto disk: one.sysmlper orbital plane pluslibrary.sysmlandconstellation.sysml. Files are staged under the output directory, recorded with their SHA-256 in.stress-model-files(itself replaced whole by rename, never truncated) before they are moved in, and a later run removes only recorded regular files that still read as recorded — a user's ownplane009.sysml, an edited one, or a directory at a recorded name survives, and an interrupted generation is retried and cleaned up.TestSatelliteNetworkSplitValidateschecks the split declares the single file's network, validates clean at one job and at several, through the batch and through the persistent workspace, and that every satisfy assertion holds across files;tools/cmd/stress-model/main_test.gopins the manifest behaviors.Where the design and the implementation differ
-validatejoined the files into one<repl>document, so per-document parallelism had nothing to parallelize; feat(repl): analyze each loaded file as a workspace document of its own #309 is the prerequisite this branch is stacked on.project.Dependencies, 0.87 s) and installing the scope trees and expanding wildcard imports before the pool (commitBatch, 1.0 s) are serial too — over 6 s of the 9.24 s on one thread. Gathering on the pool — each worker gathering its own document into the union before analysis starts — is the step left to ~5 s and is left as the follow-up; it touches the gather's locking and belongs in a change of its own.passes.Optionshas no option to disable the audit passes and none was added;BenchmarkAnalyzeSplitPerDocumentbuilds apasses.RegistryfromDefaultRegistry().Passes()minus the three and runs it over one index at one job and one per CPU.DiagnosticsAllholds the workspace's write lock across the pool, asDiagnosticsalready does across one analysis. Batch parsing happens before the lock is taken.-split-planesrefuses a destination it did not write or that was edited since, but a save intoplaneNNN.sysmlbetween that check and theos.Renameis overwritten; an editor honours no lock, so only a lock file could serialize concurrent generators, and none can close the editor window. The output directory is documented as the generator's own.Measurements
Intel Xeon Platinum 8559C, 8 CPUs, 31 GiB, no swap, Go 1.25.0 linux/amd64,GOMAXPROCS=8;/usr/bin/time -v sysml -validate -memstats; one run per row; CPU = (user + sys) / wall. Models fromgo run -C tools ./cmd/stress-model -planes 32 -satellites 50 -ground-stations 160(1 600 satellites; 18.4 MB in one file, 34 files split) and-planes 8 -satellites 25 -ground-stations 20(200 satellites, 10 files).The diagnostics of the 34-file run are byte-identical at 1/2/4/8 jobs (
md5 f342e2dfbbe27d422794fd745aa46f08), and so are the 200-satellite split's; every run reports no errors. Before the gather was shared, the 34-file split measured 129 s / 66.1 s / 38.6 s / 30.9 s at 1/2/4/8 workers, 39.1 GiB allocated, 1.98 → 7.24 GB peak RSS.CPU profile, split 1 600, 8 jobs (9.57 s wall, 34.2 s of samples,
-cpuprofile): the three audits' gather 4.5 s (13%) — OOSEM 3.6 s, identity 0.52 s, MOSA 0.37 s;NameResolutionPass.Run9.0 s (26%),W9CInheritedNameConflictPass.Run4.0 s,TypeCheckPass.Run1.2 s;repl.preparse2.45 s andOpenAllparsing on the pool,commitBatch1.0 s serial,project.Dependencies0.87 s serial;runtime.gcBgMarkWorker5.6 s (16%),runtime.scanobject5.8 s (17%). On one job (22.0 s wall) the 34 analyses are 17.1 s of samples, 3.9 s of them the gather, so a document averages 0.39 s: no single file bounds the pool.Benchmarks (
tests/stressmodel,-benchtime 3x, four planes, six files, 512 satellites):The per-document benchmark (audits left out) shows the pool's own speedup, 3.6× over six files, the largest file being about a quarter of the work.
Allocation follow-ups (listed, not implemented)
Parallelism leaves what a load allocates unchanged (7.6 GiB, 108 M objects at any job count). The allocation sites the pool does not help — the inherited-name conflict pass's per-question slices and per-base member lists,
symbols.FQNOfbuilding a string per lookup,specializationChainandAllSupertypesslices, the parser's per-reference qualified-name nodes — are recorded with their shares indocs/internals/performance.md, "What a batch of files costs", each to be measured on its own before it is changed.One parse per load is spent twice, as before: the REPL parses each file to accept it (declared names, whether it closes its own text) and the workspace parses the same bytes again as the document. The 34 split files (17 MB) parse in 1.8 s serially — ~1.8 s of the one-job 22.0 s, ~0.3 s of the eight-job wall, where it runs on the pool. Carrying the accepted tree into the workspace batch is a change to what
model.Inputowns, listed as a follow-up in the same section.Edits beside
batch.gointernal/check/passes/kit/pass.go: theBatchtype (Documents,Gathers,Source),Context.BatchandContext.InBatch.internal/check/passes/analyze.go:PrepareBatchandAnalyzeInBatchbesideAnalyzeShared; all funnel through oneanalyze(ctx, root)that runs the registry, drops escalated warnings and sorts.internal/workspace/model/workspace.go: the pool size withDefaultWorkers()(one per CPU); thebatchedset andchangescounters;invalidateLocked(names ...string)drops batch-computed diagnostics before asking the resolver;Open/Update/SetOnDiskandOpenAllinstall through oneinstallLocked(doc)— the library displacement,AddBuiltDocumentwith the document's already-built scope, and the library stand-in — thenExpandWildcardImportsonce per call;analyze(name, doc, batch)dispatches toAnalyzeInBatchwhen a batch is given andAnalyzeSharedon the persistent resolver, model and gathers otherwise. Serial incremental behavior is otherwise unchanged.internal/semantic/symbols/index.go: adding a document the index already holds removes the old one without expanding wildcard imports (the publicRemoveDocumentstill does) and records the document as changed for the persistent resolver's invalidation; a batch of N reloads expands once, as N fresh files do (TestReplacingDocumentsExpandsOnceEqualToFreshBuild).internal/check/edit/edit.go: the reindexer, which relied on that incidental expansion, now expands explicitly.internal/semantic/resolve/document.go:LinkMetadataBodies, and a body's owner refreshed — or cleared — when its definition's document is reloaded, on both paths (TestMetadataBodyOwnerFollowsTheDefinitionsDocument,TestOpenAllReloadedMetadataDefinitionReownsAnnotationBodies).internal/frontend/repl/jobs.go,cmd/sysml/{main,usage}.go,internal/frontend/usage/environment.go:-jobsalso sizes the load pool.How it was verified
gofmt -l .— nothing;go build ./...,go vet ./...(root and thetoolsmodule) — clean;python3 scripts/changelog.py check,make docs-check,make docs-counts,make man-check— clean.go test ./...andgo test -race ./...— all packages ok.OPENSYSML_REQUIRE_TRAINING_CORPUS=1 OPENSYSML_REQUIRE_PILOT_CORPORA=1 go test -count=1 ./tests/corpus -run 'TestTrainingExamples|TestPilotCorpora'— 100/100 training files clean; pilot 55/58, 95/99, 56/56, unchanged;training_examples_expected.txtuntouched, no pilot ratchet moved.TestParallelBatchValidationMatchesSerial(fixtures,examples/, four OMG corpora; one job vs N vs one-by-one),TestDiagnosticsAllAnswersInTheOrderAsked,TestDiagnosticsAllOverSomeDocumentsMatchesAskingOneByOne,TestDiagnosticsAllOverOneDocumentPreparesTheOthersBodies,TestDiagnosticsAllReadsCommentBodiesAsAnEditorDoes,TestParallelBatchLinksAnnotationsFoundThroughTheModel,TestOpenAllReplacesEarlierDocuments,TestOpenAllKeepsAChangeMadeWhileItParsed,TestOpenAllReloadedMetadataDefinitionReownsAnnotationBodies,TestWorkersSetting,TestBatchSettlesPendingGathersBeforeServingTheCacheininternal/workspace/model;TestJobsGovernLoadsincmd/sysml;TestReplacingDocumentsExpandsOnceEqualToFreshBuildininternal/semantic/symbols;TestLinkMetadataBodies*andTestMetadataBodyOwnerFollowsTheDefinitionsDocumentintests/resolve;TestSatelliteNetworkSplitValidates,BenchmarkValidateSplit,BenchmarkAnalyzeSplitPerDocumentintests/stressmodel; the sevenTestWriteSplit*manifest tests intools/cmd/stress-model.sysml -validateover the 200- and 1 600-satellite splits at-jobs1/2/4/8 produces byte-identical stdout per model; the single-file output is byte-identical to develop's.Checklist
make testandmake lintpass locallychanges/unreleased/<slug>.<section>.md, not as an edit toCHANGELOG.mdmake docs-countsrun if a gate count moved (compliance rows need nothing: the census is counted at docs build)