Repository navigation
perf!: allocate nothing per sound value in irverify and pass - #1147
Merged
Merged
Conversation
NewBigVal built a new string for every literal, through a strings.Builder, even when the result spelled the literal it was given, and read the literal's range through a Decimal whose digits joined the runs either side of the point. A literal in JSON form already now comes back as it is: the scanner records whether a point was written, and the three affixes the JSON spelling rewrites are checked where they lie. The range is read by counting digits rather than joining them, so only ParseDecimal, which keeps the digits, pays for the join. A canonical literal now costs NewBigVal nothing, and a rewrite the one string it builds. irverify asks NewBigVal of every numeric literal a document holds, and the compilers of every literal a spec writes.
irverify exempted a primitive's empty name by rendering the path of every primitive it met, with ".Name" appended, and looking each Naming that names nothing up among them. The record cost a string per primitive, leaned on the walk visiting a struct before its fields, and matched by spelling: a struct embedding a primitive beside a Name of its own shares that path, so its own missing name was exempted too. WalkPath.Field reports the struct field a path ends in, as String spells it: the field's name and the struct it is spelled on, which for a field promoted through embedded structs is the outermost embedding one. It reads the walk's own steps and allocates nothing. checkNaming now asks it of a Naming that names nothing, keeps no state for the exemption, and exempts exactly the Name of a node nameOptional lists. Alias lists are read entry by entry rather than copied into a slice, and held through one map the check keeps and empties for each list, so a list longer than a map holds unallocated no longer costs a map of its own.
checkDuplicateIDs rendered the path of every property the walk reached and joined each property's three fingerprint parts into a string, so that the report could look up a declaration's fingerprint by its path. Both strings were thrown away for every property a document holds once, and for every copy a reused component makes that matches the first. A fingerprint is now a struct of the property's own strings, kept per PropID for the first property the walk meets on it. A later property is compared with it as the walk reaches it, and only one that differs, which is the defect, has its path rendered. The walk meets properties in the order the declarations list them, so the first property is the first declaration; a test pins that ir.Property is the only node declaring a PropID, since another would repeat one unfingerprinted.
The checks that range over the type registry, the auth schemes and the namespace declaration built a path for every entry before knowing there was anything wrong with it: checkPrimIDs and checkUnions for every type or union, checkRequires for every entry, and appendSpaceViolations for every namespace. Each now spells it in the branch that reports, through one typePath helper for the type registry's spelling. memberScopeViolation joined an enum's path and the separator to compare each member against; beneath compares the two in place. checkMemberIDs also sorted every type ID to visit enums in ID order, but each violation's path names its enum and member, so Verify's own sort already orders them. checkRequires made two maps per model and checkOAuthScopes one per flow, which allocate once a model holds more than eight properties or a flow more than eight scopes. Each check now holds one map of each and empties it for the next model or flow. A test holds every function shaped like a registry check to a cost that does not grow with the entries it ranges over, and a second one holds the list it measures to every such function the package declares.
checkStringLists made a map for each []string field it holds to a rule, which allocates once a list holds more than eight entries. The check now keeps one map and empties it for each list, as checkNaming does for alias lists. A test also holds checkBigVals to costing nothing for a literal in canonical form, which ir.NewBigVal now hands back as it is, through both carriers: a Value's Num and a bound held by pointer.
AllocatingVisits walked a document with one visitor standing for all of them, so what any one pruned was pruned for every one. A visitor that prunes at the root, as one visiting nothing does, then hid the whole document from the others: handed every irverify walking check, the measurement reached no value below the root and reported a document clean with a path rendered for every property. Each visitor is now wrapped on its own and walked as ir.WalkValues walks it, so it prunes for itself alone, and a visitor gathering cycles is told of them. A value is listed once for each visitor allocating there. A visit allocating nothing is measured once rather than twenty times, as no later measurement can find fewer, which keeps a walk with many visitors affordable.
Each check that fixed a per-value allocation pins its own cost, but a check with no such test, or one written later, could still build a string for every value it reaches. One test now walks a document the OpenAPI compiler produces from extensions-x.yaml, which writes an extension on every object OpenAPI admits one on, and holds the walking checks together to no allocating visit. checkReferentialIntegrity is left out of it: ir.IDReferences keeps a reference whose target the walk has yet to declare with its path, until the walk is over, which is that type's design and pinned in ir. Verify's doc now states what a value costs a check and that exception.
checkInferred, which main added beside this branch's changes, asks ir.Inferred.IsCanonical of every marker list, and that reads a list through a map of its own: free for the lists a compiler writes, one allocation for a list of more than eight names, as its doc chooses. Verify's doc said a value with nothing wrong costs a check nothing beyond the paths ir reads, which that list contradicts; it now names the set as the other exception.
checkRawPayloads asked jsontext.Value.IsValid of each Unmodeled and RawConfig payload, which borrows a decoder from a sync.Pool and hands it back. A race build drops a share of what a pool is handed, so there a payload cost an allocation at random. The walking checks' cost test flagged an Unmodeled map of many entries for it under make coverage's -race build, while every other build measured nothing. The check now reads payloads through one jsontext.Decoder and one bytes.Reader it resets for each, which costs nothing once the decoder's buffer holds the largest payload, in every build. A test holds it to IsValid's verdict over every string of up to five bytes JSON's grammar turns on, and the shapes no short string reaches; a fuzz comparison of the two found no disagreement in five million inputs. A payload object with many members still costs the decoder an allocation per name it keeps to refuse a duplicate, as IsValid did, and Verify's doc names it beside ir's paths and the inferred-marker set.
checkDocument built the path of every source in the table before holding it to either rule. The issue counted it free, since the string never escapes and stays short enough to build on the stack, but strconv.Itoa builds the string for an index of 100 or more, so a document read from more than 100 sources paid an allocation for each source past the hundredth. The path is now spelled in the branch that reports, as every other registry check spells its own. The registry checks' cost test measured 10 entries against 20, below the index at which that starts, so it could not see this; it now measures 100 against 200.
TestVerify_OptionalOwnerExemptsOnlyItsOwnName said the descendant axis, a nameless node inside an exempt one, had no pair to measure with while ir.Primitive is the only exemption. nameExempt takes its table, so TestNameExempt_IsTheOptionalNodesOwnNameAlone now holds a node of its own inside the listed one to the rule, and the comment points there.
visitMeasurements' comment held the chance of a pool drop in every measurement negligible for the handful of pools a visit reaches, which leaves out a visit handing back dozens: checkRawPayloads' Unmodeled maps were one until it stopped pooling. It now says so, and two comments describing that cost read in the present.
AllocatingVisits said it lists paths in walk order, which holds for up to 64 visitors: past that ir.WalkValues walks again, so the list runs walk by walk. A walk-path test said a primitive's Name is promoted through two embedded structs where its own table counts three. The walking checks' cost test leaned on extensions-x.yaml holding none of the costs Verify's doc excepts, the one it leaves out aside, without saying so; a later edit to the spec adding a large extension object would redden it for a documented cost.
AllocatingVisits measures a visit only once the walk has made it, so a cost a check pays on its first meeting with a value goes unmeasured. That is meant for a memo kept per type, but it hides state kept per value too: checkDuplicateIDs rendering and keeping a path for each PropID it meets, the cost this branch removed, passed every test in the package. TestWalkChecks_AFirstMeetingCostsNothing compiles a spec of 100 and of 200 components, each an operation and the schema it returns, and requires the walking checks to allocate less than one more per component added, with the walk's own allocations taken out. The checks add 2 in the plain build and in the race and coverage build; the planted path adds about 400. AllocatingVisits' doc now says what a first meeting hides. Verify's no longer says a sound value costs a check nothing while checkDuplicateIDs keeps a fingerprint per PropID: what it may cost is the step a map the check keeps grows by.
TestCheckNaming_ExemptsAPrimitivesOwnNameWhereverItSits pins that a primitive embedded in a struct beside a Name of its own keeps no exemption, since the walk spells the primitive's promoted Name on the embedding struct, so its name claimed an exemption its second case shows does not hold. It is renamed for the two positions it holds, and its doc now says both Names at the shared path are reported.
WalkPath.Field said a field Go promotes through an embedded struct is spelled on the embedding struct. The walk also reaches a field Go shadows, and Field reports that one on the outermost struct too, as TestWalkPath_FieldIsTheFieldThePathEndsIn pins. Say so there, and in the naming test that measures the case, which called the shadowed Name promoted. AllocatingVisits said the zero WalkPath renders empty without saying why that matters: it renders without allocating, so a visit measured with it would hide every render.
TestPayloadReader_ReadsAsIsValidDoes reached the decoder's nesting limit only from far past it, at 20,000 levels. The reader skips a value token by token, where jsontext.Value.IsValid reads it whole, and the library bounds the two paths separately, so the rows that would show them parting are the ones either side of the limit: an array and an object nested 10,000 deep, which both accept, and 10,001, which both refuse.
checkRequires, checkOAuthScopes, checkStringLists and the alias check each read every collection through one map they emptied with clear, so a collection cost no map of its own. Go never shrinks a map: clear returns at once on an empty one, and otherwise walks every table it grew to. One huge collection therefore taxed every later one. A model listing 1,000,000 Requires names beside 200,000 models listing two each took checkRequires 490 ms, against 166 ms making a map per model as before. Each site now reads through a reusedMap, which empties its map in place while the last collection left it at most 1,024 entries and replaces it past that. The same input takes 127 ms, and a collection within the bound still costs no allocation. A test refuses a bare clear anywhere else in the package, so a later site goes through the bound too.
ir.IDDeclarations spelled the path of every declaration, and ir.IDReferences that of every reference met before its target, though a check reports few of them, and none on a sound document. On the golden spec's IR they were 140,062 of the 143,400 allocations irverify.Verify made once its checks built nothing else. PathLog keeps a walk's paths without spelling any. Each is stored as the steps it shares with the path recorded before it and the steps after those, so recording one copies a step or two and allocates nothing but the log's growth, by five halves at a time. A path is spelled only when a replay of the log is asked for its String. IDDeclarations keeps its paths there and yields them through Paths, and IDReferences keeps its references' there and spells, in Unresolved, only those still unresolved. irverify's duplicate and namespace checks spell only the declarations they report. WalkPath.IsMapKey tells a map key from its value without spelling the path, for a caller that keeps one and not the other. BREAKING CHANGE: ir.IDDeclaration has no Path. A caller spells a declaration's path from IDDeclarations.Paths, which yields each with its position in List.
pass.Validate built for nearly every value it held to a rule: a location formatted for every payload, error case, HTTP binding and pagination input, a map per model, enum, union, binding and channel, the sorted keys of every mapping, encoding map and SuccessStatus, and the path of each PropID the walk kept until its property was read. On the golden spec's IR, which it finds clean, that was most of its 85,700 allocations. Each rule now keeps a location as its parts and spells it in the branch that reports, reads its collections through a reusedMap or a key list it keeps for the run, and keeps a PropID's path in an ir.PathLog, spelled only if the PropID dangles. A mapping's and an encoding map's keys are still asked in sorted order, so the questions the shared facts index is asked do not follow map iteration. Every output is as it was. TestValidate_ASoundValueCostsNothing holds the walk and every rule family, the index built, to costing less than one allocation more per entity when the document doubles. The index itself still allocates per type it indexes and per question it walks (#1150), as the doc on the rule families says.
The rule on measuring a render through a real walk told a reader to give each visitor a call of its own to irtest.AllocatingVisits, so that no other visitor's pruning hid a visit. AllocatingVisits now walks each visitor as it would walk alone, so that reason no longer holds, and the rule said nothing of what the helper cannot see: it measures only a repeat of each visit, so a path kept on every first meeting passed it. The rule now says both, and names the doubling test that holds a first meeting.
WalkPath's doc said a path is valid until the call it was passed to returns and that a visitor keeping one keeps its String. A PathLog now replays paths too, each valid until the next is yielded, and is the way to keep one without spelling it.
checkMapping asks a mapping's keys in sorted order, so its reports within one discriminator do not follow Go's map iteration, and this change rewrote that sort to run in a list the check keeps. No test held it: iterating unsorted left the package green, as #821 records for this site. TestValidate_MappingDiagnosticsAreDeterministic maps five keys out of order onto no variant and requires their reports in key order across nine runs, which the messages show since every one is placed at the union.
TestValidate_ASoundValueCostsNothing said each entity reaches every rule family, but its document named no server, so the server-index check ran over nothing, and kept no PropID for checkPropIDRefs to settle. A location spelled for every channel's servers passed it. Each channel and the service now name a server, and each operation paginates to an item property the walk meets before its model, which the test requires is kept. Both plants now redden it.
9 tasks done
The cost test for a first meeting measured the walk alone. What a check does once the walk is over went unmeasured: checkDuplicateIDs and checkIDSpaces spell a declaration's path only for one they report, and spelling every path again took irverify.Verify on the golden spec's IR from 3.3k allocations to 144k with every test green. The test now measures Verify as a whole, and is renamed TestVerify_ACleanComponentCostsNothing for what it holds, so the reports and the registry checks answer to it beside the walk. Restoring either report's eager paths reddens it, as a path kept on each first meeting still does.
checkIDSpaces places each ID outside the declaration by its position among the declarations, and no case checked where two such IDs were reported, so placing all of them at the first one's node passed. A case with two, a declared ID between them, now pins each to its own node. beneath compares a member's path with its enum's in two steps, and dropping the first, that the path opens with the enum's, passed too: no case's member path opened with an empty segment. A member whose path opens with one before the enum's now pins it.
Both callers of declarations.pathsAt return before asking for no position, so its own test for an empty set decided nothing: removing it changed no result and reddened no test. The zero-value guard beside it stays.
Validate reads each union's variants, each binding's bound parameters and locations, and each model's claimed names through one map per check, emptied for each collection. A site that stopped emptying it would carry one collection into the next, and nothing in pass noticed for four of them: a mapping onto the previous union's variant passed, a parameter the first binding placed was never unbound in a second, one placed once by each binding was bound twice, and a name another model claimed made an alias ambiguous. Each now has a case whose first collection would change the verdict on the second. The bindings case also pins that each report names its own binding, the second, which no case with one binding could.
Validate spells a report's location from its parts only once there is a report, and no response-index, encoding-key or message-binding case reported anything past the first binding, content, response, error case or listed message, so a location spelling index 0 for a later one passed. A second binding with a response index out of range, a second content, response and error case each carrying an encoding key no property answers to, and a second listed message its channel does not carry now pin each report to its own position.
PathLog shares a step with the path recorded before it when the two are equal, owner included. A walk shows one value at one path, so within a walk the owner never decides; it does once a log is fed walks of two types, and comparing steps without it passed every test. A log fed a Document's IRVersion and then a Channel's ID, each its struct's first field, now pins it. IDDeclarations grows its list five halves at a time, as the log grows its own, and restoring append there passed too. A test beside the log's now holds the list to fewer growths than doubling would take.
The doc on Validate's rule families, and the sound-value cost test's, said a family sorts a collection only for a diagnostic. checkMapping and appendUnknownPartDiags sort every mapping's and encoding map's keys, so the facts index is asked in a fixed order; they sort into a list kept for the run, which costs nothing per value. Both now say what holds: a family allocates only for a diagnostic.
This was referenced Oct 29, 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.
Summary
irverifyandpass.Validatebuilt a string or a collection for nearly every value they held to a rule, whether or not the value broke it: a path or a location to place a finding that never came, a prefix to compare against, a key to look a fingerprint up by, a rendering to compare with, a map or a sorted key list per collection.irdid the same for them, spelling the path of every declaration and of every reference met before its target. On the golden spec's compiled IR, which both find clean, those were nearly all ofVerify's andValidate's allocations. This change makes a value with nothing wrong cost a check nothing, keeps a path a check may report unspelled until it reports it, and holds every check to that.On the golden spec (Cloudflare
openapi.json@1a8429dc, the revision #1139 measured), one run of each over the compiled IR:main@acf5e3bairverify.Verifyallocationsirverify.Verifybytes allocatedpass.Validateallocationspass.Validatebytes allocatedVerifyValidateThe ranges are three runs of each binary. Instructions are each binary run with 21 and with 1 call, the difference over 20, three alternating rounds;
Validate's two ranges overlap. Wall time swung by more than the difference under the machine's load, so it is not quoted.What remains is made once per run: the maps the walk and each check keep and the logs that keep paths, all of which grow in steps, about 94 allocations the JSON decoder makes for the 75 names of the one large object payload the spec holds, to refuse a duplicate, and, for
Validate, the facts index it asks, which allocates per type it indexes (#1150).Each site
irverify:checkDuplicateIDs,fingerprintOffingerprintstruct of the property's own strings, kept per PropID for the first property the walk meets, and how many it has met there; a later one is compared as it is reached, and only one that differs is marked, by its PropID and occurrencecheckPrimIDs,checkUnions,checkRequires,appendSpaceViolations"types[" + id + "]"and its kintypePathhelper for the type registrycheckDocumentstrconv.Itoabuilds the string for an index of 100 or morememberScopeViolationenumPath + "/", to compare a prefixbeneath, comparing the two in placeappendBigValir.NewBigVal's rendering, built through astrings.Builder, plus a join of the digit runs to read the rangeir.NewBigValreturns a literal already in JSON form as it is, and reads the range without joining the digitscheckNamingPrimitive, recording its exemptionir.WalkPath.Field, at aNamingthat names nothingcheckRawPayloadsjsontext.Value.IsValid'ssync.Poolper payload, which a race build drops from, so there a payload cost an allocation at randomjsontext.Decoderandbytes.Reader, reset for each payload, held toIsValid's verdictcheckRequires,checkOAuthScopes,checkStringLists, alias checkingNaming, which allocates past eight entriesreusedMapper check, emptied for each collectionpass.Validate:forEachPayload,forEachErrorPayloadpayloadSite.wherein the branch that reportsappendUnknownPartDiags,checkMappingappendSuccessStatusDiags,checkResponseIndicesSuccessStatuskeyscheckHTTPParamBinding,checkParamReferencesreusedMapeach and one list, the location spelled only for a diagnosticcheckDuplicateWireNames,ambiguousAliases,checkDuplicateMemberIDs,checkUnionDiscriminator,appendUncarriedMessageDiagsreusedMapeach, the location spelled only for a diagnosticpropIDSitesir.PathLog, spelled only for a PropID that danglesir:IDDeclarationsPathLogand yielded unspelled byPaths;checkDuplicateIDsandcheckIDSpacesspell only the declarations they reportIDReferencesPathLog;Unresolvedspells only those still unresolvedDesign choices
NewBigValat its owner, not a new predicate. irverify: checks build strings for values they find nothing wrong with #1139 proposed comparing a literal with its canonical form as it is read, asir.HasCanonicalWordsdoes for a name. Returning the literal itself when the JSON spelling would not change it gives every caller the saving, the compilers' calls for every numeric literal a spec writes included, and adds no API. The scanner now records whether a point was written, the one affix it left no trace of.WalkPath.Fieldinstead of "the nextNamingbelongs to the primitive". That proposal depends on the walk visiting a struct before its fields, cannot tell when a walk cut short between the two hands the exemption to an unrelatedNaming, and breaks a visit repeated with the same arguments, whichirtest.AllocatingVisitsrelies on.Fieldreports the struct field a path ends in asStringspells it, a field reached through an embedded struct on the outermost struct embedding it, reads the walk's own steps and allocates nothing. The exemption is now exact as well: a struct embedding aPrimitivebeside aNameof its own shares thatName's path, and the old path record exempted it too.irverify's. Recording it whereir.IDDeclarationsrecords the declaration would put a check's policy intoir. The check keeps the first fingerprint per PropID and counts the properties it meets there, so the n-th property met on a PropID is the n-th declaration of it, andTestPropIDs_AreDeclaredByPropertiesAlonepins thatir.Propertyis the only node declaring a PropID, since another would repeat one unfingerprinted.ir.PathLogstores each path a walk shows as the steps it shares with the path recorded before it and the steps after those, so recording one copies a step or two. It grows by five halves at a time:appendgrows a large slice by a quarter, which copied the log several times over and cost about 50 MB more on the golden spec.slices.Growgrows about as far, but a race build makes two allocations for each of its growths, which the gate's cost tests count. Walking the document again to spell a reported path was the alternative; it adds a second walk to every run that reports one, andpass's walk asks its context, which a second walk would not.Requiresnames beside 200,000 listing two tookcheckRequires490 ms with an unbounded map, 166 ms with a map per model, and 127 ms with the bound. Both packages keep the same smallreusedMaptype, and each holds everyclearin it to the type.passcould have collected only the failing keys of a mapping or an encoding map and sorted those, but the rules ask the shared facts index of each key, and the index's cost turns on the order it is asked in. Sorting each map's keys in a list the check keeps leaves that order as it was and allocates nothing per map.What holds it
irverifysite has a test in the style ofTestCheckDeclaredIDs_VisitingCostsNoAllocation, throughirtest.AllocatingVisits, andirhasTestNewBigVal_AllocatesOnlyToRewrite,TestIDDeclarations_AllocatesNothingPerDeclaration,TestIDReferences_KeepingCostsNoAllocationandTestPathLog_RecordingCostsNoAllocation.TestPayloadReader_ReadsAsIsValidDoesholds the payload reader toIsValid's verdict over every string of up to five bytes drawn from those JSON's grammar turns on, and either side of the nesting limit.TestRegistryChecks_ACleanEntryCostsNothingholds every check that ranges a registry to a cost that does not grow with its entries, measured at 100 entries against 200 so an index's string crosses into the onesstrconvbuilds, andTestRegistryChecks_ListsEveryRegistryCheckderives every function shaped like one from the package's sources.TestWalkChecks_ACleanValueCostsNothingholds everythingVerify's walk carries, the declarations and every walking check, to no allocating visit on the document compiled fromextensions-x.yaml.TestVerify_ACleanComponentCostsNothingholds what a visit test cannot see, a check's first meeting with a value and its report once the walk is over, by measuring all ofVerify: a spec of 200 components costs less than one allocation more per component than one of 100.TestValidate_ASoundValueCostsNothingholdspass's walk and each rule family, the facts index built, to the same, over a document of 50 and of 100 entities that each reach every family with collections wider than a map holds unallocated.TestValidate_MappingDiagnosticsAreDeterministicholds the sorted ordercheckMappingkeeps a mapping's keys in, which this change rewrote and pass: pin Validate's diagnostic order wherever it ranges a map #821 lists as unpinned; the response-index and encoding-key sorts it also rewrote were pinned already, and still redden when removed.TestReusedMap_EmptiesInPlaceUpToTheLimitpins the bound in each package, andTestReusedMaps_AreEmptiedThroughReusedMapAlonerefuses a bareclearanywhere else in it. Each map a check reuses has a case whose first collection would change the verdict on the second; the onespasslacked areTestValidate_UnionMappingIsHeldToItsOwnVariants,TestValidate_ParamBinding_EachBindingIsJudgedAloneand a case ofTestValidate_UnambiguousAliasIsClean.TestPathLog_KeepsASharedStepOnce,TestPathLog_KeepsTheTypeEachFieldIsSpelledOnandTestPathLog_GrowsGeometricallypin the log's sharing, the type each step is spelled on and its growth, andTestIDDeclarations_GrowsTheListGeometricallythe declaration list's growth.TestValidate_ParamBinding_EachBindingIsJudgedAloneandTestValidate_ResponseIndexOutOfRangeIsPlacedAtItsBinding, the second content, response and error case inTestValidate_EncodingKeyIsPlacedWhereItSits, the second listed message inTestValidate_MessageNotCarriedIsPlacedAtItsPosition, and the second of two IDs outside the declaration inTestVerify_EachIDOutsideTheDeclarationIsReportedAtItsOwnNode.That first-meeting test is what found
irtest.AllocatingVisitsmeasuring nothing. It walked with one visitor standing for all of them, so what any one pruned was pruned for all, andcheckIDSpaces, which visits nothing, prunes at the root. Handed every walking check, it reported a document clean with a path planted on every property. It now wraps each visitor and walks it asir.WalkValuesdoes, so each prunes for itself alone, and a visit allocating nothing is measured once rather than twenty times, which is what makes measuring every check affordable. Under-racethe same test then flaggedcheckRawPayloads.Breaking
ir.IDDeclarationhas noPath. A caller spells a declaration's path fromIDDeclarations.Paths, which yields each with its position inList, as aWalkPathvalid until the next.irtest.AllocatingVisitsshows each visitor the walk it would be shown alone rather than pruning for all what one prunes, and lists a value once for each visitor allocating there. A visitor gathering cycles is now told of them, which it never was, and a nil visitor is skipped, where the old walk called it and panicked; past 64 visitors the list runs walk by walk, asir.WalkValuestraverses. The one caller that passed several visitors before this change, inpass, found the same paths under the new walk, and now finds none, sincepass's walk keeps no path spelled.ir.PathLog,WalkPath.Field,WalkPath.IsMapKeyandIDDeclarations.Pathsare new.Out of scope
Validateasks.irfacts.Indexallocates per type it indexes and per question it walks (irfacts: a walked question allocates, and the hierarchy index allocates per type #1150), soTestValidate_ASoundValueCostsNothingbuilds it before measuring, and the doc onpass's rule families says so.ir.Inferred.IsCanonicalreads a list through a map of its own, free for the lists a compiler writes and one allocation for a list of more than eight names, as its doc chooses.Verify's doc names it.jsontext.Value.IsValiddid. The check needs the refusal, and the allocations are the library's.ir.WalkValues' cost, not a check's.Test plan
make gateon the final tree: both CI jobs pass on it, all 16,199 statements covered.TestMain_AStopSignalAfterTheRunComesTooLateincmd/morphic, which this change does not touch, failed one earlier run under the coverage build; it fails 5 of 10 runs onmainthe same way, and test: a covered child's metadata rename fails under parallel signal subtests #1151 tracks it.go vet ./...andgo test ./ir/... ./pass ./internal/archteston its own.morphic compile --verifygives byte-identical stdout, stderr and exit status onmainand this branch for the golden spec, the 340 YAML and JSON files undertestdataand 1,840 real-world OpenAPI specs.irverifysites (1,260 documents, 3,676 violations), and again, with three documents built to reach every rule family, with defects aimed at every changedpassrule and at duplicate and misplaced declarations (1,440 documents, 1,864 diagnostics under 16 codes, every code the changed sites report among them, and 45,106 violations).Verify's andValidate's output is byte-identical onmain@acf5e3ba and the branch.AllocatingVisitsover all 210 clean documents the corpus compiles to: 802 onmain(479 incheckDuplicateIDs, 249 incheckNaming, 65 incheckBigVals, 9 incheckReferentialIntegrity), and none on this branch, the declarations' collector included.PathLog's sharing, replay, roots and growth toreusedMap's bound, the duplicate check's occurrence count andIsMapKey. 103 defects, every one killed by the test written for it; four no-op controls survive. A later round planted every arm and guard the change adds, each reused map left unemptied between collections, each report's position and each growth policy: 18 survived every test, and each now has a test that reddens on it.pathsAt's check for an empty set, which both callers make first, is gone. Three survivors change no output: spelling every declaration's path once a document has a violation, which is a defective document's cost;appendGrowing'smax, which matters only while a log is shorter than one path; andembedded's kind test, since only a field step has an owner.-race -covermode=atomic, five times each, asmake coverageruns them.Closes #1139.