From 2122fc80f9cff40d301f789ef2265747166e6a23 Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Mon, 12 Oct 2026 00:27:32 +0200 Subject: [PATCH 1/6] Filter wide exact-key lookup with metered cached fingerprints --- .../Release1.1/BenchmarkReview.md | 56 ++++++++++++++ .../Architecture/Release1.1/Decisions.md | 18 +++++ Documentation/Qualification/README.md | 2 +- Scripts/ci.py | 2 +- Sources/JSON/Value/JSONKey.swift | 8 +- Sources/JSON/Value/JSONObject.swift | 21 +++++- .../Release1_1/LookupIncrementTests.swift | 74 +++++++++++++++++++ Tools/Release1_1Probes/LookupBenchmark.swift | 50 +++++++++++++ Tools/Release1_1Probes/README.md | 18 +++++ 9 files changed, 244 insertions(+), 5 deletions(-) create mode 100644 Documentation/Architecture/Release1.1/BenchmarkReview.md create mode 100644 Tests/JSONTests/Release1_1/LookupIncrementTests.swift create mode 100644 Tools/Release1_1Probes/LookupBenchmark.swift diff --git a/Documentation/Architecture/Release1.1/BenchmarkReview.md b/Documentation/Architecture/Release1.1/BenchmarkReview.md new file mode 100644 index 0000000..9e0b159 --- /dev/null +++ b/Documentation/Architecture/Release1.1/BenchmarkReview.md @@ -0,0 +1,56 @@ +# Initial exact-key lookup review + +This increment filters the tail of a linear lookup using existing immutable key +fingerprints. Objects below 32 members retain the scalar loop. Wide objects compare +the first eight members scalarly, then hash the query once under the active meter. +Every candidate hit still confirms exact UTF-8 identity. No object representation, +publication path, duplicate detector, capacity weight or public API changes here. +The array-backed AVL and controlled hash prototypes remain experiment subjects. + +The matched production-stage comparison used MacBookPro18,2, Apple M1 Max, 32 GiB, +macOS 27.0, Xcode 27.2 / SDK 27.2 and Swift 6.4 in language mode 6, release `-O`. +`Tools/Release1_1Probes/LookupBenchmark.swift` covers ten widths, three key shapes, +first/middle/last/missing queries and the first filtered member. Each run has three +warmups and nine measured samples, twenty lookups with result checks per sample: +129 query cells and 1161 recorded samples. The balanced/hash prototype measurements +are a separate corpus; their timing is not presented as production improvement. + +Two sequential matched production pairs were retained, together with an earlier +candidate measurement. The table uses the repeated pair. Times are medians in +milliseconds for twenty lookups, including identical result checks; work counts +are owned package units, excluding the benchmark's result-check work. + +| Shape / width / query | Scalar ms | Filter ms | Change | Scalar / filter work | +| --- | ---: | ---: | ---: | ---: | +| ASCII / 8 / last | 0.018041 | 0.018083 | +0.23% | 1620 / 1620 | +| ASCII / 16 / middle | 0.020166 | 0.020167 | approximately 0% | 1820 / 1820 | +| ASCII / 32 / first filtered | 0.020209 | 0.023750 | +17.52% | 1820 / 2020 | +| ASCII / 32 / miss | 0.017584 | 0.019375 | +10.19% | 1280 / 1400 | +| Prefix / 32 / first filtered | 0.230042 | 0.252583 | +9.80% | 24320 / 27020 | +| ASCII / 1024 / last | 1.657416 | 0.525750 | -68.28% | 144420 / 42140 | +| Prefix / 1024 / last | 24.807875 | 0.848333 | -96.58% | 2704420 / 67140 | +| ASCII / 10000 / last | 16.837083 | 5.013375 | -70.22% | 1422220 / 401180 | +| Prefix / 10000 / last | 248.313000 | 6.156417 | -97.52% | 26422220 / 426180 | + +The unchanged 10% review trigger applies. Accept the observed boundary cost for +this focused draft increment: query hashing adds ten work units to the first +filtered ASCII hit and six to the short missing key, approximately 0.18 and 0.09 +microseconds per lookup in the repeated pair. Later wide hits materially reduce +owned comparisons. This is an explicit tradeoff, not an assertion that every +lookup improves. Keep the scalar path and qualify adaptive metadata separately. + +Timings are host observations. An initial width-16 ASCII timing outlier disappeared +in the unchanged-code repeat, and wide scalar timings varied between runs; retain +both raw sets and variance instead of attributing those changes to the algorithm. +Empty-loop timings are at timer/call-overhead scale and establish no throughput +claim. This stage retains no owned buffer and reports zero logical retained bytes; +it measures neither process RSS nor allocator counts. Construction, whole-root +admission, native keyed encoding, thermal/energy effects and mobile crossovers +remain unqualified R1/R5 work. Algorithm costs may change resource failure points; +all finite profiles and counter version/weights remain unchanged. + +The historical stream median 1.0749626803 seconds versus 0.968493986 seconds +(+10.9932%) retains its existing accepted disposition and 10% trigger. This lookup +stage gives no stream attribution or fresh full-workload release qualification. +Raw samples, source/input hashes, command receipts and medians/variance are retained +in the milestone return bundle, with compiler, SDK, CI and fixture evidence. diff --git a/Documentation/Architecture/Release1.1/Decisions.md b/Documentation/Architecture/Release1.1/Decisions.md index a0e65ac..9307d07 100644 --- a/Documentation/Architecture/Release1.1/Decisions.md +++ b/Documentation/Architecture/Release1.1/Decisions.md @@ -96,3 +96,21 @@ bundle cardinalities, three targeted Thread Sanitizer cases, 49 exact cap probes independent corpus counts and exclusions remain intact. R0's filtered run requires exactly seven cases. New platform declarations must later update the exact macOS-only manifest expectation in the same focused change. + +## First production increment + +Select only the narrow cached-fingerprint filter described in +[BenchmarkReview](BenchmarkReview.md). The below-32-member scalar path and first +eight scalar probes remain available. A query hash, each member probe, fingerprint +comparison and exact candidate comparison consume the same noncopyable meter; +there is no new table, cached mutable state or owned index allocation. The two +additional behavior cases exercise public presence/Pointer/equality, insertion +order, forced collision confirmation in the production comparator, inclusive and +cumulative work exhaustion and cancellation during actual lookup work. + +The current 10% performance review trigger is unchanged; accept the measured +first-filtered-hit and short-miss cost for this limited draft change. Full adaptive +index publication/construction and keyed-builder/mobile measurement remain R1/R5, +not completed by these tests. CI now expects JSONTests 63 and total 131 functions; +the seven-case R0 subset, baseline parameterized invocations and all other exact +cardinality/49-cap checks remain unchanged. diff --git a/Documentation/Qualification/README.md b/Documentation/Qualification/README.md index a412659..68f9f4a 100644 --- a/Documentation/Qualification/README.md +++ b/Documentation/Qualification/README.md @@ -8,7 +8,7 @@ Safety and DocC lanes run separately. Unsupported hosts fail preflight. The 1.0 baseline has 122 Swift Testing functions across five bundles and 173 parameterized invocations. The first 1.1 qualification milestone adds seven authored prototype cases in JSONTests (54 to 61), for 129 functions, with the existing 173 parameterized invocations retained -and seven additional nonparameterized cases. No baseline case or corpus outcome is removed. One public cap test exercises all 49 metrics; +and seven additional nonparameterized cases. The first production lookup increment adds two further cases (JSONTests 63, total 131 functions). No baseline case or corpus outcome is removed. One public cap test exercises all 49 metrics; the independent runner adds nine budget/report vectors. There are three separate JSON/JSONSchema consumer tests. All discoveries must be nonzero. diff --git a/Scripts/ci.py b/Scripts/ci.py index 90a5b01..d9967af 100644 --- a/Scripts/ci.py +++ b/Scripts/ci.py @@ -68,7 +68,7 @@ def check_result(command, code, log): counts = [int(n) for n in re.findall(r'Test run with (\d+) tests? in[^\n]*passed', log)] passed = code == 0 if command[:2] == ['swift', 'test']: - expected = [7] if R0_TESTS in command else [3] if 'Tools/MCPReadiness' in command else [1] if 'Tools/Qualification' in command else [1, 1, 1] if 'thread' in command else [8, 11, 12, 37, 61] + expected = [7] if R0_TESTS in command else [3] if 'Tools/MCPReadiness' in command else [1] if 'Tools/Qualification' in command else [1, 1, 1] if 'thread' in command else [8, 11, 12, 37, 63] passed = passed and sorted(counts) == expected if 'Tools/Qualification' in command: probes = re.findall(r'^CAP_PROBE (\w+) (\S+)$', log, re.MULTILINE) diff --git a/Sources/JSON/Value/JSONKey.swift b/Sources/JSON/Value/JSONKey.swift index a1fb303..9cd9dc0 100644 --- a/Sources/JSON/Value/JSONKey.swift +++ b/Sources/JSON/Value/JSONKey.swift @@ -6,7 +6,7 @@ public struct JSONKey: Sendable, Hashable, ExpressibleByStringLiteral { /// The decoded scalar spelling. public let string: String - private let fingerprint: UInt64 + package let fingerprint: UInt64 /// Owns the supplied decoded scalar spelling without normalization or a resource quota. /// /// - Parameters: @@ -23,6 +23,12 @@ public struct JSONKey: Sendable, Hashable, ExpressibleByStringLiteral { for byte in string.utf8 { try meter.work(subsystem: subsystem); hash = (hash ^ UInt64(byte)) &* 1099511628211 } fingerprint = hash } + /// A candidate filter only: every fingerprint hit still compares exact scalar bytes. + package func matches(_ spelling: String, fingerprint: UInt64, meter: inout JSONWorkMeter) throws -> Bool { + try meter.work() + guard self.fingerprint == fingerprint else { return false } + return try JSONComparison.textEquals(string, spelling, meter: &meter) + } /// Constructs a trusted literal value without a resource budget; bounded operations admit it later. /// /// - Parameters: diff --git a/Sources/JSON/Value/JSONObject.swift b/Sources/JSON/Value/JSONObject.swift index ff13b29..20ae988 100644 --- a/Sources/JSON/Value/JSONObject.swift +++ b/Sources/JSON/Value/JSONObject.swift @@ -82,9 +82,26 @@ public struct JSONObject: Sendable, Hashable, RandomAccessCollection { /// - hasher: The caller's randomized Swift hasher. public func hash(into hasher: inout Hasher) { JSON.object(self).hash(into: &hasher) } package func lookup(_ key: String, meter: inout JSONWorkMeter) throws -> JSON? { - for member in members { + if members.count < 32 { + for member in members { + try meter.work() + if try JSONComparison.textEquals(member.key.string, key, meter: &meter) { return member.value } + } + return nil + } + var fingerprint: UInt64? + for (position, member) in members.enumerated() { try meter.work() - if try JSONComparison.textEquals(member.key.string, key, meter: &meter) { return member.value } + // Keep small objects and early hits on the scalar path. Hash the query + // once, under this invocation's meter, only when a wide scan continues. + if position == 8 { + fingerprint = try JSONKey(key, meter: &meter).fingerprint + } + if let fingerprint { + if try member.key.matches(key, fingerprint: fingerprint, meter: &meter) { return member.value } + } else if try JSONComparison.textEquals(member.key.string, key, meter: &meter) { + return member.value + } } return nil } diff --git a/Tests/JSONTests/Release1_1/LookupIncrementTests.swift b/Tests/JSONTests/Release1_1/LookupIncrementTests.swift new file mode 100644 index 0000000..4ec74ca --- /dev/null +++ b/Tests/JSONTests/Release1_1/LookupIncrementTests.swift @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: Apache-2.0 +import Synchronization +import Dispatch +import Testing +@testable import JSON + +@Test func wideLookupPreservesExactKeysOrderAndCollisionConfirmation() throws { + let prefix = String(repeating: "p", count: 64) + let padding = (0..<32).map { JSONMember(prefix + String($0), value: .boolean(false)) } + let special = ["", "0", "/", "~", "a.b", "\u{e9}", "e\u{301}"] + let members = padding + special.map { JSONMember($0, value: .string($0)) } + [JSONMember("nullable", value: .null)] + let object = try JSONObject(members: members) + let value = JSON.object(object) + for key in special { + let selected = try value.requiredMember(key) + #expect(selected.stringValue?.utf8.elementsEqual(key.utf8) == true) + #expect(try JSONPointer(tokens: [key]).resolve(in: value) == selected) + } + #expect(try value.requiredMember("nullable") == .null) + #expect(try value.presence(of: "absent") == .absent) + #expect(try value.presence(of: "nullable") == .null) + #expect(object.members.map(\.key) == members.map(\.key)) + #expect(try JSONComparison.equals(value, .object(JSONObject(members: members.reversed())))) + // Force a fingerprint hit at the production candidate comparator. Distinct + // decoded scalar spellings must still fail full UTF-8 confirmation. + let composed = JSONKey("\u{e9}") + var meter = try JSONWorkMeter(limits: .balanced, domain: .core) + #expect(try !composed.matches("e\u{301}", fingerprint: composed.fingerprint, meter: &meter)) + #expect(try composed.matches("\u{e9}", fingerprint: composed.fingerprint, meter: &meter)) +} + +private final class LookupCancellationCheckpoint: Sendable { + let ready = Mutex(false) + let resume = DispatchSemaphore(value: 0) + func observe(_ work: UInt64) { + guard work >= 1400 else { return } + let stop = ready.withLock { flag in if flag { return false }; flag = true; return true } + if stop { resume.wait() } + } +} + +@Test func wideLookupEnforcesInclusiveCumulativeWorkAndInnerCancellation() async throws { + let prefix = String(repeating: "p", count: 128) + let members = (0..<256).map { JSONMember(prefix + String($0), value: .boolean(true)) } + let object = try JSONObject(members: members, limits: .offline) + let key = members.last!.key.string + let value = JSON.object(object) + var meter = try JSONWorkMeter(limits: .offline, domain: .core) + try JSONAdmission.text(key, key: true, meter: &meter) + #expect(try object.lookup(key, meter: &meter) == .boolean(true)) + let inclusive = meter.statistics()[.workUnits] + var limits = JSONLimits.offline; limits[.workUnits] = inclusive + #expect(try value.requiredMember(key, limits: limits) == .boolean(true)) + limits[.workUnits] = inclusive - 1 + do { + _ = try value.requiredMember(key, limits: limits) + Issue.record("Lookup ignored its inclusive work ceiling") + } catch let failure as JSONFailure { #expect(failure.resource?.metric == .workUnits) } + var shared = try JSONWorkMeter(limits: limits, domain: .core) + #expect(try object.lookup(key, meter: &shared) == .boolean(true)) + #expect(throws: JSONFailure.self) { try object.lookup(key, meter: &shared) } + let progress = LookupCancellationCheckpoint() + let task = Task.detached { () throws -> UInt64 in + var meter = try JSONWorkMeter(limits: .offline, domain: .core) + meter.checkpointObserver = { progress.observe($0) } + do { + for _ in 0..<1000 { _ = try object.lookup(key, meter: &meter) } + return 0 + } catch is CancellationError { return meter.statistics()[.workUnits] } + } + while !progress.ready.withLock({ $0 }) { await Task.yield() } + task.cancel(); progress.resume.signal() + #expect(try await task.value >= 1400) +} diff --git a/Tools/Release1_1Probes/LookupBenchmark.swift b/Tools/Release1_1Probes/LookupBenchmark.swift new file mode 100644 index 0000000..457027c --- /dev/null +++ b/Tools/Release1_1Probes/LookupBenchmark.swift @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 +import Foundation +import CryptoKit +@testable import JSON + +private struct Sample: Encodable { + let width: Int, shape: String, query: String, sample: Int + let iterations: Int, seconds: Double, work: UInt64, logicalPeak: UInt64 + let inputSHA256: String +} +@main struct LookupBenchmark { + static func main() throws { + var samples: [Sample] = [] + for width in [0,1,4,8,16,32,64,256,1024,10000] { + for shape in ["ascii", "prefix", "unicode"] { + let keys = (0.. 8 { queries.append(("first-filtered", keys[8])) } + for (name, key) in queries { + for sample in -3..<9 { + var meter = try JSONWorkMeter(limits: .offline, domain: .core) + let clock = ContinuousClock(); let start = clock.now + for _ in 0..<20 { + let value = try object.lookup(key, meter: &meter) + precondition(name == "miss" ? value == nil : value?.stringValue?.utf8.elementsEqual(key.utf8) == true) + } + let duration = start.duration(to: clock.now).components + if sample >= 0 { + let stats = meter.statistics() + samples.append(Sample(width: width, shape: shape, query: name, sample: sample, iterations: 20, + seconds: Double(duration.seconds) + Double(duration.attoseconds)/1e18, + work: stats[.workUnits], logicalPeak: stats.peakLogicalRetainedBytes, inputSHA256: digest)) + } + } + } + } + } + let encoder = JSONEncoder(); encoder.outputFormatting = [.sortedKeys] + try encoder.encode(samples).write(to: URL(fileURLWithPath: CommandLine.arguments[1])) + print("LOOKUP_BENCHMARK samples=\(samples.count) warmups=3 iterations=20 measured_samples=9") + } +} diff --git a/Tools/Release1_1Probes/README.md b/Tools/Release1_1Probes/README.md index e6e6590..74afbec 100644 --- a/Tools/Release1_1Probes/README.md +++ b/Tools/Release1_1Probes/README.md @@ -16,3 +16,21 @@ Use Scripts/qualify_release_1_1.py through Scripts/ci.py where applicable. Index measurements must run on an otherwise idle release-native host. Samples identify input hashes, finite policy, work, logical peak and refusals. They do not measure isolated allocator allocations, process RSS or physical mobile throughput. + +The small production increment is reviewed in +[BenchmarkReview](../../Documentation/Architecture/Release1.1/BenchmarkReview.md). +Its deterministic lookup-stage corpus can be repeated after building the real +release testable module on the supported toolchain: + +```sh +swift test -c release --filter wideLookup +xcrun swiftc -swift-version 6 -O -parse-as-library \ + -I .build/out/Products/Release -L .build/out/Products/Release -lJSON \ + Tools/Release1_1Probes/LookupBenchmark.swift -o .build/lookup-benchmark +.build/lookup-benchmark .build/lookup-samples.json +``` + +For A/B, build the baseline libraries in a separate worktree and compile the same +benchmark source against those libraries. Record both core source identities and +the benchmark/input hashes. Run measurements while local verification builds are +idle; preserve all samples, refusal outcomes and repeated-run variance. From 5d01f8873ff6b1d0d50224a38d0960be6eea9f8d Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Mon, 12 Oct 2026 00:58:39 +0200 Subject: [PATCH 2/6] Cancel lookup probes at owned checkpoints without blocking workers --- .../Release1_1/LookupIncrementTests.swift | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/Tests/JSONTests/Release1_1/LookupIncrementTests.swift b/Tests/JSONTests/Release1_1/LookupIncrementTests.swift index 4ec74ca..dcfe860 100644 --- a/Tests/JSONTests/Release1_1/LookupIncrementTests.swift +++ b/Tests/JSONTests/Release1_1/LookupIncrementTests.swift @@ -1,6 +1,4 @@ // SPDX-License-Identifier: Apache-2.0 -import Synchronization -import Dispatch import Testing @testable import JSON @@ -29,16 +27,6 @@ import Testing #expect(try composed.matches("\u{e9}", fingerprint: composed.fingerprint, meter: &meter)) } -private final class LookupCancellationCheckpoint: Sendable { - let ready = Mutex(false) - let resume = DispatchSemaphore(value: 0) - func observe(_ work: UInt64) { - guard work >= 1400 else { return } - let stop = ready.withLock { flag in if flag { return false }; flag = true; return true } - if stop { resume.wait() } - } -} - @Test func wideLookupEnforcesInclusiveCumulativeWorkAndInnerCancellation() async throws { let prefix = String(repeating: "p", count: 128) let members = (0..<256).map { JSONMember(prefix + String($0), value: .boolean(true)) } @@ -59,16 +47,15 @@ private final class LookupCancellationCheckpoint: Sendable { var shared = try JSONWorkMeter(limits: limits, domain: .core) #expect(try object.lookup(key, meter: &shared) == .boolean(true)) #expect(throws: JSONFailure.self) { try object.lookup(key, meter: &shared) } - let progress = LookupCancellationCheckpoint() let task = Task.detached { () throws -> UInt64 in var meter = try JSONWorkMeter(limits: .offline, domain: .core) - meter.checkpointObserver = { progress.observe($0) } + meter.checkpointObserver = { work in + if work >= 1400 { withUnsafeCurrentTask { $0?.cancel() } } + } do { for _ in 0..<1000 { _ = try object.lookup(key, meter: &meter) } return 0 } catch is CancellationError { return meter.statistics()[.workUnits] } } - while !progress.ready.withLock({ $0 }) { await Task.yield() } - task.cancel(); progress.resume.signal() #expect(try await task.value >= 1400) } From bf37941955f0e1689a0d258414b5fa95b69ccf69 Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Mon, 12 Oct 2026 09:09:36 +0200 Subject: [PATCH 3/6] Maintain exact-key indexes during bounded construction and native coding --- .../Architecture/Release1.1/Index.md | 48 ++++++++ Scripts/ci.py | 2 +- Sources/JSON/Codec/JSONTokenMachine.swift | 26 ++-- .../JSON/Coding/JSONEncodingContainers.swift | 16 +-- Sources/JSON/Coding/JSONValueEncoder.swift | 44 +++++-- .../Construction/JSONObjectAccumulator.swift | 15 ++- Sources/JSON/Pointer/JSONEditing.swift | 8 +- Sources/JSON/Value/JSONComparison.swift | 3 +- Sources/JSON/Value/JSONExactKeyIndex.swift | 111 ++++++++++++++++++ Sources/JSON/Value/JSONObject.swift | 58 +++++---- Sources/JSONPatch/JSONMergePatch.swift | 4 +- .../Inspection/JSONSchemaNodeView.swift | 8 +- .../Output/JSONValidationOutputAdapter.swift | 4 +- .../Release1_1/IndexPublicationTests.swift | 91 ++++++++++++++ .../ConstructionBenchmark.swift | 103 ++++++++++++++++ Tools/Release1_1Probes/LookupBenchmark.swift | 5 + 16 files changed, 462 insertions(+), 84 deletions(-) create mode 100644 Documentation/Architecture/Release1.1/Index.md create mode 100644 Sources/JSON/Value/JSONExactKeyIndex.swift create mode 100644 Tests/JSONTests/Release1_1/IndexPublicationTests.swift create mode 100644 Tools/Release1_1Probes/ConstructionBenchmark.swift diff --git a/Documentation/Architecture/Release1.1/Index.md b/Documentation/Architecture/Release1.1/Index.md new file mode 100644 index 0000000..4b6c057 --- /dev/null +++ b/Documentation/Architecture/Release1.1/Index.md @@ -0,0 +1,48 @@ +# Exact-key construction and publication + +Ordered members remain the public collection and writer authority. Each published +object also owns immutable exact-key metadata. The index stores decoded JSONKey +identity and insertion positions, never canonically equating String dictionary keys. + +The R0 release-native comparisons favored controlled open addressing for wide +objects. The current candidate uses a linear index below 32 keys, then a power-of-two +table with occupancy at most one half. Lookups keep the scalar path for small +objects and the first eight members. A wide lookup hashes its query once, probes +the table and confirms exact UTF-8 on every candidate hit. Collision scans are +finite and metered; this is no adversarial constant-time guarantee. + +Each metadata capacity slot retains the existing Version 1 `indexSlot` weight of +32 logical bytes. Unused capacity is included. New capacity is admitted while old +capacity remains live; copying, initialization, rehashing and exact probes consume +the same meter. Rejected growth releases its replacement reservation and leaves +prior metadata valid. Logical capacities do not assert allocator or RSS ceilings. + +The shared grammar maintains its own duplicate index at decoded key recognition. +A duplicate still fails at the second encoded key's start before its colon/value. +Materialized objects maintain separate metadata as keys arrive. Checked +construction and the accumulator maintain their index before publication; consuming +nonthrowing `finish` transfers it without search, allocation or a fresh budget. +Editing, native materialization and schema inspection/output build metered metadata +before returning a value. Empty Merge Patch targets use an explicit empty index. + +Grammar duplicate capacity and DOM metadata are distinct live owners. Full owned +graph admission includes the published index's actual logical capacity and member +slots separately. The metric and weight meanings stay at Version 1; capacity/work +counts legitimately differ with the algorithm, and defaults are not raised. + +Native keyed encoding now keeps one invocation-owned mutable storage object and +maintained exact index. Updating a field no longer extracts and rewrites the entire +member array. Member growth admits old/new capacities together. Materialization +charges key traversal and builds immutable output metadata under the same meter. +This source observation alone makes no claim of one allocation per field. + +`IndexPublicationTests` covers all JSON publication routes, scalar-distinct keys, +forced collisions, failed rehash, duplicate timing and cancellation. The separate +release construction benchmark measures checked, parser, accumulator and native +keyed paths over 120 cells, using the matched finite offline profile. It records +resource refusals separately, and allocator snapshots separately from timing and +logical retention. A successful SDK build does not establish a mobile crossover. + +Full R1 closure requires the actual final native/safety/consumer measurements and +review receipts. The test-only AVL/scalar alternatives remain qualification +references; no table, platform or throughput result is inferred from this prose. diff --git a/Scripts/ci.py b/Scripts/ci.py index d9967af..b44a6c9 100644 --- a/Scripts/ci.py +++ b/Scripts/ci.py @@ -68,7 +68,7 @@ def check_result(command, code, log): counts = [int(n) for n in re.findall(r'Test run with (\d+) tests? in[^\n]*passed', log)] passed = code == 0 if command[:2] == ['swift', 'test']: - expected = [7] if R0_TESTS in command else [3] if 'Tools/MCPReadiness' in command else [1] if 'Tools/Qualification' in command else [1, 1, 1] if 'thread' in command else [8, 11, 12, 37, 63] + expected = [7] if R0_TESTS in command else [3] if 'Tools/MCPReadiness' in command else [1] if 'Tools/Qualification' in command else [1, 1, 1] if 'thread' in command else [8, 11, 12, 37, 67] passed = passed and sorted(counts) == expected if 'Tools/Qualification' in command: probes = re.findall(r'^CAP_PROBE (\w+) (\S+)$', log, re.MULTILINE) diff --git a/Sources/JSON/Codec/JSONTokenMachine.swift b/Sources/JSON/Codec/JSONTokenMachine.swift index 6836a1f..4702b13 100644 --- a/Sources/JSON/Codec/JSONTokenMachine.swift +++ b/Sources/JSON/Codec/JSONTokenMachine.swift @@ -11,7 +11,7 @@ package struct JSONMachineEvent: Sendable { package struct JSONTokenMachine { package enum State: Sendable { case ready, reading, complete, failed, finished } private enum Expectation { case objectFirstKey, objectKey, colon, objectValue, objectEnd, arrayFirstValue, arrayValue, arrayEnd } - private struct Frame { var expectation: Expectation; var count: UInt64 = 0; var keys: [String] = [] } + private struct Frame { var expectation: Expectation; var count: UInt64 = 0; var keys = JSONExactKeyIndex() } private var lexer = JSONLexer() private let topLevelArrayStreaming: Bool package init(topLevelArrayStreaming: Bool = false, offset: UInt64 = 0) { @@ -76,17 +76,17 @@ package struct JSONTokenMachine { } frames.removeLast(); try meter.release(JSONLogicalWeights.frame) // Key payload remains in a materialized value or in the caller's event; index slots are grammar-owned. - for _ in frame.keys { try meter.work(); try meter.release(JSONLogicalWeights.indexSlot) } + try meter.release(frame.keys.retainedBytes) if frames.isEmpty { rootComplete = true; state = .complete } return .init(kind: object ? .endObject : .endArray, start: token.start, end: token.end) case .string(let text) where expectingKey: let index = frames.count - 1 - for previous in frames[index].keys { - if try JSONComparison.textEquals(previous, text, meter: &meter) { throw failure(token, .duplicateKey) } + let key = try JSONKey(text, meter: &meter) + guard try frames[index].keys.insert(key, position: Int(frames[index].count), meter: &meter) else { + throw failure(token, .duplicateKey) } frames[index].count = try meter.adding(.objectMembers, frames[index].count, 1) - try meter.retain(JSONLogicalWeights.indexSlot) - frames[index].keys.append(text); frames[index].expectation = .colon + frames[index].expectation = .colon return .init(kind: .key(text), start: token.start, end: token.end) case .leftObject, .leftArray: try beginValue(token, meter: &meter) @@ -138,7 +138,7 @@ package struct JSONTokenMachine { } package struct JSONValueAssembler { - private struct Frame { var object: Bool; var key: String?; var members: [JSONMember] = []; var values: [JSON] = [] } + private struct Frame { var object: Bool; var key: JSONKey?; var members: [JSONMember] = []; var values: [JSON] = []; var index = JSONExactKeyIndex() } private var stack: [Frame] = [] package init() {} package mutating func consume(_ event: JSONMachineEvent, meter: inout JSONWorkMeter) throws -> JSON? { @@ -149,10 +149,14 @@ package struct JSONValueAssembler { let object: Bool if case .startObject = event.kind { object = true } else { object = false } stack.append(Frame(object: object)); return nil - case .key(let key): stack[stack.count - 1].key = key; return nil + case .key(let key): + let last = stack.count - 1 + let key = try JSONKey(key, meter: &meter) + guard try stack[last].index.insert(key, position: stack[last].members.count, meter: &meter) else { throw JSONFailure(.syntax, code: .duplicateKey) } + stack[last].key = key; return nil case .endObject, .endArray: let frame = stack.removeLast(); try meter.release(JSONLogicalWeights.frame) - let value: JSON = frame.object ? .object(JSONObject(admitted: frame.members)) : .array(frame.values) + let value: JSON = frame.object ? .object(JSONObject(admitted: frame.members, index: frame.index)) : .array(frame.values) return try add(value, meter: &meter) case .scalar(let value): try meter.retain(JSONLogicalWeights.node) @@ -164,8 +168,8 @@ package struct JSONValueAssembler { let index = stack.count - 1 if stack[index].object { guard let key = stack[index].key else { throw JSONFailure(.syntax, code: .invalidOperation) } - try meter.retain(JSONLogicalWeights.member + JSONLogicalWeights.indexSlot) - stack[index].members.append(JSONMember(try JSONKey(key, meter: &meter), value: value)); stack[index].key = nil + try meter.retain(JSONLogicalWeights.member) + stack[index].members.append(JSONMember(key, value: value)); stack[index].key = nil } else { try meter.retain(JSONLogicalWeights.arraySlot); stack[index].values.append(value) } return nil } diff --git a/Sources/JSON/Coding/JSONEncodingContainers.swift b/Sources/JSON/Coding/JSONEncodingContainers.swift index 608d1b7..0659879 100644 --- a/Sources/JSON/Coding/JSONEncodingContainers.swift +++ b/Sources/JSON/Coding/JSONEncodingContainers.swift @@ -5,21 +5,9 @@ struct JSONKeyedEncodingContainer: KeyedEncodingContainerProtoco var codingPath: [any CodingKey] { encoder.codingPath } private func child(_ key: any CodingKey) throws -> JSONNativeEncoder { try encoder.state.check() - guard case .keyed(var members) = encoder.box.storage else { throw JSONFailure(.configuration, code: .invalidOperation) } + guard case .keyed(let storage) = encoder.box.storage else { throw JSONFailure(.configuration, code: .invalidOperation) } let child = try encoder.child(key) - var found: Int? - for index in members.indices { - try encoder.state.meter.work() - if try JSONComparison.textEquals(members[index].0.string, key.stringValue, meter: &encoder.state.meter) { found = index; break } - } - for _ in members { try encoder.state.meter.work() } - if let found { members[found] = (try JSONKey(key.stringValue, meter: &encoder.state.meter), child.box) } - else { - try encoder.state.meter.require(.objectMembers, UInt64(members.count) + 1) - try encoder.state.meter.retain(JSONLogicalWeights.member + JSONLogicalWeights.indexSlot) - members.append((try JSONKey(key.stringValue, meter: &encoder.state.meter), child.box)) - } - encoder.box.storage = .keyed(members) + try storage.set(JSONKey(key.stringValue, meter: &encoder.state.meter), box: child.box, meter: &encoder.state.meter) return child } mutating func encodeNil(forKey key: Key) throws { try child(key).set(.null) } diff --git a/Sources/JSON/Coding/JSONValueEncoder.swift b/Sources/JSON/Coding/JSONValueEncoder.swift index 2a24b01..412da71 100644 --- a/Sources/JSON/Coding/JSONValueEncoder.swift +++ b/Sources/JSON/Coding/JSONValueEncoder.swift @@ -45,8 +45,36 @@ public struct JSONValueEncoder: Sendable { return result } } +/// Exclusive mutable keyed storage avoids extracting/copying the whole member +/// array for every field. This object belongs to one native coding invocation. +package final class JSONKeyedEncodingStorage { + package private(set) var members: [(JSONKey, JSONEncodingBox)] = [] + private var index = JSONExactKeyIndex() + private var capacity = 0 + package func set(_ key: JSONKey, box: JSONEncodingBox, meter: inout JSONWorkMeter) throws { + if let position = try index.find(key, meter: &meter) { + try meter.work(); members[position] = (key, box); return + } + try meter.require(.objectMembers, UInt64(members.count) + 1) + if members.count == capacity { + let (doubled, overflow) = capacity.multipliedReportingOverflow(by: 2) + guard !overflow else { throw JSONFailure(.resource, code: .counterOverflow) } + let next = min(Int(meter.limits[.objectMembers]), max(1, doubled)) + let (bytes, byteOverflow) = UInt64(next).multipliedReportingOverflow(by: JSONLogicalWeights.member) + guard !byteOverflow else { throw JSONFailure(.resource, code: .counterOverflow) } + try meter.retain(bytes) + do { try meter.work(UInt64(members.count) + 1) } + catch { try meter.release(bytes); throw error } + members.reserveCapacity(next) + try meter.release(UInt64(capacity) * JSONLogicalWeights.member) + capacity = next + } + guard try index.insert(key, position: members.count, meter: &meter) else { throw JSONFailure(.syntax, code: .duplicateKey) } + members.append((key, box)) + } +} package final class JSONEncodingBox { - package enum Storage { case unset, value(JSON), keyed([(JSONKey, JSONEncodingBox)]), unkeyed([JSONEncodingBox]) } + package enum Storage { case unset, value(JSON), keyed(JSONKeyedEncodingStorage), unkeyed([JSONEncodingBox]) } package var storage: Storage = .unset package func materialize(state: JSONCodingState) throws -> JSON { enum Step { case node(JSONEncodingBox), object([JSONKey]), array(Int) } @@ -58,9 +86,11 @@ package final class JSONEncodingBox { switch node.storage { case .unset: throw EncodingError.invalidValue("unset", .init(codingPath: [], debugDescription: "No value was encoded")) case .value(let value): results.append(value) - case .keyed(let members): - pending.append(.object(members.map { $0.0 })) - for (_, box) in members.reversed() { try state.meter.work(); try state.meter.retain(16); pending.append(.node(box)) } + case .keyed(let storage): + var keys: [JSONKey] = [] + for (key, _) in storage.members { try state.meter.work(); try state.meter.retain(JSONLogicalWeights.arraySlot); keys.append(key) } + pending.append(.object(keys)) + for (_, box) in storage.members.reversed() { try state.meter.work(); try state.meter.retain(16); pending.append(.node(box)) } case .unkeyed(let boxes): pending.append(.array(boxes.count)) for box in boxes.reversed() { try state.meter.work(); try state.meter.retain(16); pending.append(.node(box)) } @@ -68,9 +98,9 @@ package final class JSONEncodingBox { case .object(let keys): var members: [JSONMember] = [] for (key, value) in zip(keys, results.suffix(keys.count)) { - try state.meter.work(); try state.meter.retain(JSONLogicalWeights.member + JSONLogicalWeights.indexSlot); members.append(JSONMember(key, value: value)) + try state.meter.work(); try state.meter.retain(JSONLogicalWeights.member); members.append(JSONMember(key, value: value)) } - results.removeLast(keys.count); results.append(.object(JSONObject(admitted: members))) + results.removeLast(keys.count); results.append(.object(try JSONObject.indexed(admitted: members, meter: &state.meter))) case .array(let count): var values: [JSON] = [] for value in results.suffix(count) { try state.meter.work(); try state.meter.retain(JSONLogicalWeights.arraySlot); values.append(value) } @@ -91,7 +121,7 @@ package final class JSONNativeEncoder: Encoder { state.latch { try state.check() switch box.storage { - case .unset: box.storage = .keyed([]) + case .unset: box.storage = .keyed(JSONKeyedEncodingStorage()) case .keyed: break default: throw EncodingError.invalidValue("container", .init(codingPath: codingPath, debugDescription: "Conflicting containers")) } diff --git a/Sources/JSON/Construction/JSONObjectAccumulator.swift b/Sources/JSON/Construction/JSONObjectAccumulator.swift index dd1b568..b7df0c4 100644 --- a/Sources/JSON/Construction/JSONObjectAccumulator.swift +++ b/Sources/JSON/Construction/JSONObjectAccumulator.swift @@ -4,6 +4,7 @@ /// Duplicate decoded keys fail; growth and payload work share the builder budget. public struct JSONObjectAccumulator: ~Copyable { private var members: [JSONMember] = [] + private var index = JSONExactKeyIndex() private var meter: JSONWorkMeter /// Creates an exclusive finite builder and admits its outer object frame. /// @@ -27,17 +28,15 @@ public struct JSONObjectAccumulator: ~Copyable { public mutating func append(_ key: String, value: JSON) throws { try meter.require(.objectMembers, UInt64(members.count) + 1) try JSONAdmission.text(key, key: true, meter: &meter, owned: true) - for member in members { - if try JSONComparison.textEquals(member.key.string, key, meter: &meter) { - throw JSONFailure(.syntax, code: .duplicateKey) - } - } + let admittedKey = try JSONKey(key, meter: &meter) + guard try index.find(admittedKey, meter: &meter) == nil else { throw JSONFailure(.syntax, code: .duplicateKey) } try JSONAdmission.value(value, meter: &meter, owned: true, depth: 1) - try meter.retain(JSONLogicalWeights.member + JSONLogicalWeights.indexSlot) - members.append(JSONMember(try JSONKey(key, meter: &meter), value: value)) + try meter.retain(JSONLogicalWeights.member) + guard try index.insert(admittedKey, position: members.count, meter: &meter) else { throw JSONFailure(.syntax, code: .duplicateKey) } + members.append(JSONMember(admittedKey, value: value)) } /// Consumes the builder and transfers its already admitted result. - public consuming func finish() -> JSONObject { JSONObject(admitted: members) } + public consuming func finish() -> JSONObject { JSONObject(admitted: members, index: index) } } extension JSONObject { diff --git a/Sources/JSON/Pointer/JSONEditing.swift b/Sources/JSON/Pointer/JSONEditing.swift index 168220d..e17e366 100644 --- a/Sources/JSON/Pointer/JSONEditing.swift +++ b/Sources/JSON/Pointer/JSONEditing.swift @@ -36,17 +36,17 @@ package enum JSONEditing { switch change { case .remove: continue case .add(let value), .replace(let value): - try meter.retain(JSONLogicalWeights.member + JSONLogicalWeights.indexSlot); members.append(JSONMember(member.key, value: value)); continue + try meter.retain(JSONLogicalWeights.member); members.append(JSONMember(member.key, value: value)); continue } } - try meter.retain(JSONLogicalWeights.member + JSONLogicalWeights.indexSlot); members.append(member) + try meter.retain(JSONLogicalWeights.member); members.append(member) } if !found { guard case .add(let value) = change else { throw JSONFailure(.pointer, code: .missingTarget) } try meter.require(.objectMembers, UInt64(members.count) + 1) - try meter.retain(JSONLogicalWeights.member + JSONLogicalWeights.indexSlot); members.append(JSONMember(token, value: value)) + try meter.retain(JSONLogicalWeights.member); members.append(JSONMember(token, value: value)) } - return .object(JSONObject(admitted: members)) + return .object(try JSONObject.indexed(admitted: members, meter: &meter)) case .array(let array): let add: Bool if case .add = change { add = true } else { add = false } diff --git a/Sources/JSON/Value/JSONComparison.swift b/Sources/JSON/Value/JSONComparison.swift index 394086c..22694cc 100644 --- a/Sources/JSON/Value/JSONComparison.swift +++ b/Sources/JSON/Value/JSONComparison.swift @@ -43,10 +43,11 @@ package enum JSONAdmission { case .object(let object): let containerDepth = try meter.adding(.maxDepth, parentDepth, 1) try meter.require(.objectMembers, UInt64(object.count)) + if owned { try meter.retain(object.exactIndex.retainedBytes) } for member in object.reversed() { try text(member.key.string, key: true, meter: &meter, owned: owned) try meter.work(); try meter.retain(JSONLogicalWeights.frame) - if owned { try meter.retain(JSONLogicalWeights.member + JSONLogicalWeights.indexSlot) } + if owned { try meter.retain(JSONLogicalWeights.member) } stack.append((member.value, containerDepth)) } } diff --git a/Sources/JSON/Value/JSONExactKeyIndex.swift b/Sources/JSON/Value/JSONExactKeyIndex.swift new file mode 100644 index 0000000..ab77da6 --- /dev/null +++ b/Sources/JSON/Value/JSONExactKeyIndex.swift @@ -0,0 +1,111 @@ +// SPDX-License-Identifier: Apache-2.0 +/// Invocation-owned while building; immutable after publication. Fingerprints +/// only select candidates. Every hit confirms the complete decoded UTF-8 key. +package struct JSONExactKeyIndex: Sendable { + private struct Entry: Sendable { let key: JSONKey; let position: Int } + private var linear: [Entry] = [] + private var table: [Entry?] = [] + private var capacity = 0 + private var count = 0 + private let fingerprintMask: UInt64 + package init(fingerprintMask: UInt64 = .max) { self.fingerprintMask = fingerprintMask } + package var retainedBytes: UInt64 { UInt64(capacity) * JSONLogicalWeights.indexSlot } + private func same(_ a: JSONKey, _ b: JSONKey, meter: inout JSONWorkMeter) throws -> Bool { + try meter.work() + guard a.fingerprint & fingerprintMask == b.fingerprint & fingerprintMask else { return false } + return try JSONComparison.textEquals(a.string, b.string, meter: &meter) + } + private func slot(_ key: JSONKey, in slots: [Entry?], meter: inout JSONWorkMeter) throws -> (Int, Int?) { + var index = Int(key.fingerprint & fingerprintMask & UInt64(slots.count - 1)) + for _ in slots.indices { + try meter.work() + guard let entry = slots[index] else { return (index, nil) } + if try same(entry.key, key, meter: &meter) { return (index, entry.position) } + index = (index + 1) & (slots.count - 1) + } + throw JSONFailure(.configuration, code: .invalidOperation) + } + package func find(_ key: JSONKey, meter: inout JSONWorkMeter) throws -> Int? { + if !table.isEmpty { return try slot(key, in: table, meter: &meter).1 } + for entry in linear { + if try same(entry.key, key, meter: &meter) { return entry.position } + } + return nil + } + package func find(_ key: JSONKey) -> Int? { + if table.isEmpty { return linear.first { $0.key == key }?.position } + var index = Int(key.fingerprint & fingerprintMask & UInt64(table.count - 1)) + for _ in table.indices { + guard let entry = table[index] else { return nil } + if entry.key == key { return entry.position } + index = (index + 1) & (table.count - 1) + } + return nil + } + private func bytes(_ slots: Int, meter: inout JSONWorkMeter) throws -> UInt64 { + let (bytes, overflow) = UInt64(slots).multipliedReportingOverflow(by: JSONLogicalWeights.indexSlot) + guard !overflow else { throw JSONFailure(.resource, code: .counterOverflow) } + try meter.require(.retainedBytes, bytes) + return bytes + } + /// No mutation occurs until all throwing probes, growth/copies and charges + /// complete. A rejected append therefore preserves valid prior metadata. + package mutating func insert(_ key: JSONKey, position: Int, meter: inout JSONWorkMeter) throws -> Bool { + if try find(key, meter: &meter) != nil { return false } + try meter.require(.objectMembers, UInt64(count) + 1) + let entry = Entry(key: key, position: position) + if count < 31 { + if count == capacity { + let next = min(Int(meter.limits[.objectMembers]), max(1, capacity * 2)) + let nextBytes = try bytes(next, meter: &meter) + try meter.retain(nextBytes) + // reserveCapacity can copy the old buffer; charge its traversal + // while both old and replacement capacities are live. + do { try meter.work(UInt64(count) + 1) } + catch { try meter.release(nextBytes); throw error } + linear.reserveCapacity(next) + try meter.release(retainedBytes) + capacity = next + } else { try meter.work() } + linear.append(entry); count += 1 + return true + } + if table.isEmpty || count >= table.count / 2 { + let (next, overflow) = max(32, table.count).multipliedReportingOverflow(by: 2) + guard !overflow else { throw JSONFailure(.resource, code: .counterOverflow) } + let nextBytes = try bytes(next, meter: &meter) + try meter.retain(nextBytes) + var replacement: [Entry?] = [] + replacement.reserveCapacity(next) + do { + for _ in 0.. Self { + var index = JSONExactKeyIndex() + for position in members.indices { + guard try index.insert(members[position].key, position: position, meter: &meter) else { + throw JSONFailure(.syntax, code: .duplicateKey) + } + } + return Self(admitted: members, index: index) } package static func checked(members: [JSONMember], meter: inout JSONWorkMeter) throws -> Self { try meter.require(.objectMembers, UInt64(members.count)) var output: [JSONMember] = [] + var index = JSONExactKeyIndex() for member in members { try JSONAdmission.text(member.key.string, key: true, meter: &meter, owned: true) - for previous in output { - if try JSONComparison.textEquals(previous.key.string, member.key.string, meter: &meter) { - throw JSONFailure(.syntax, code: .duplicateKey) - } + guard try index.insert(member.key, position: output.count, meter: &meter) else { + throw JSONFailure(.syntax, code: .duplicateKey) } try JSONAdmission.value(member.value, meter: &meter, owned: true, depth: 1) - try meter.retain(JSONLogicalWeights.member + JSONLogicalWeights.indexSlot) + try meter.retain(JSONLogicalWeights.member) output.append(member) } try meter.charge(.nodes); try meter.retain(JSONLogicalWeights.node) try meter.require(.maxDepth, 1) - return Self(admitted: output) + return Self(admitted: output, index: index) } /// The first member index; equal to endIndex for an empty object. public var startIndex: Int { members.startIndex } @@ -52,15 +62,15 @@ public struct JSONObject: Sendable, Hashable, RandomAccessCollection { /// - Parameters: /// - position: A valid insertion-order member index. public subscript(position: Int) -> JSONMember { members[position] } - /// Looks up an exact decoded-scalar key with unmetered linear traversal. + /// Looks up an exact decoded-scalar key using private unmetered metadata. /// /// Returns nil for absence and JSON.null for a present null. Use bounded JSON /// member access when untrusted traversal requires work accounting. /// /// - Parameters: /// - key: The exact decoded-scalar member key, without normalization. - public subscript(key: JSONKey) -> JSON? { members.first { $0.key == key }?.value } - /// Looks up an exact decoded-scalar key with unmetered linear traversal. + public subscript(key: JSONKey) -> JSON? { exactIndex.find(key).map { members[$0].value } } + /// Looks up an exact decoded-scalar key using private unmetered metadata. /// /// Returns nil for absence and JSON.null for a present null. Use bounded JSON /// member access when untrusted traversal requires work accounting. @@ -82,28 +92,16 @@ public struct JSONObject: Sendable, Hashable, RandomAccessCollection { /// - hasher: The caller's randomized Swift hasher. public func hash(into hasher: inout Hasher) { JSON.object(self).hash(into: &hasher) } package func lookup(_ key: String, meter: inout JSONWorkMeter) throws -> JSON? { - if members.count < 32 { - for member in members { - try meter.work() - if try JSONComparison.textEquals(member.key.string, key, meter: &meter) { return member.value } - } - return nil - } - var fingerprint: UInt64? - for (position, member) in members.enumerated() { + // Preserve the qualified scalar tiny/early-hit path before hashing. + for member in members.prefix(members.count < 32 ? members.count : 8) { try meter.work() - // Keep small objects and early hits on the scalar path. Hash the query - // once, under this invocation's meter, only when a wide scan continues. - if position == 8 { - fingerprint = try JSONKey(key, meter: &meter).fingerprint - } - if let fingerprint { - if try member.key.matches(key, fingerprint: fingerprint, meter: &meter) { return member.value } - } else if try JSONComparison.textEquals(member.key.string, key, meter: &meter) { + if try JSONComparison.textEquals(member.key.string, key, meter: &meter) { return member.value } } - return nil + guard members.count >= 32 else { return nil } + let query = try JSONKey(key, meter: &meter) + return try exactIndex.find(query, meter: &meter).map { members[$0].value } } /// Returns an admitted object with a member added or replaced in place. /// diff --git a/Sources/JSONPatch/JSONMergePatch.swift b/Sources/JSONPatch/JSONMergePatch.swift index 4aea5c9..5c4b683 100644 --- a/Sources/JSONPatch/JSONMergePatch.swift +++ b/Sources/JSONPatch/JSONMergePatch.swift @@ -45,7 +45,7 @@ public struct JSONMergePatch: Sendable { return .init(result: patch, statistics: meter.statistics()) } try meter.retain(JSONLogicalWeights.frame) - var stack = [Frame(target: value.objectValue == nil ? .object(JSONObject(admitted: [])) : value, patch: root, returnKey: nil)] + var stack = [Frame(target: value.objectValue == nil ? .object(JSONObject(empty: ())) : value, patch: root, returnKey: nil)] while !stack.isEmpty { // Retire replaced immutable targets and operation-local scratch. The active frames // still own their targets; re-admit those snapshots without refunding charged work. @@ -78,7 +78,7 @@ public struct JSONMergePatch: Sendable { } } else if case .object(let nested) = member.value { try meter.require(.maxDepth, UInt64(stack.count) + 1); try meter.retain(JSONLogicalWeights.frame) - let target: JSON = existing?.objectValue == nil ? .object(JSONObject(admitted: [])) : existing! + let target: JSON = existing?.objectValue == nil ? .object(JSONObject(empty: ())) : existing! stack.append(Frame(target: target, patch: nested, returnKey: member.key)) } else { try meter.resetValueBounds(); try JSONAdmission.value(member.value, meter: &meter, owned: true) diff --git a/Sources/JSONSchema/Inspection/JSONSchemaNodeView.swift b/Sources/JSONSchema/Inspection/JSONSchemaNodeView.swift index c3ca178..20e68b0 100644 --- a/Sources/JSONSchema/Inspection/JSONSchemaNodeView.swift +++ b/Sources/JSONSchema/Inspection/JSONSchemaNodeView.swift @@ -50,21 +50,21 @@ public struct JSONSchemaNodeView: Sendable { try meter.work(); try meter.retain(JSONLogicalWeights.arraySlot); keywords.append(member.key) for name in annotationNames { if try JSONComparison.textEquals(member.key.string, name, meter: &meter) { - try meter.retain(JSONLogicalWeights.member + JSONLogicalWeights.indexSlot); annotations.append(member); break + try meter.retain(JSONLogicalWeights.member); annotations.append(member); break } } if try JSONSchemaPlan.Keyword.classify(member.key,meter: &meter) == nil { - try meter.retain(JSONLogicalWeights.member + JSONLogicalWeights.indexSlot); annotations.append(member) + try meter.retain(JSONLogicalWeights.member); annotations.append(member) } for name in constraintNames { if try JSONComparison.textEquals(member.key.string, name, meter: &meter) { - try meter.retain(JSONLogicalWeights.member + JSONLogicalWeights.indexSlot); constraints.append(member); break + try meter.retain(JSONLogicalWeights.member); constraints.append(member); break } } } } return .init(raw: raw, documentPointer: pointer, retrievalURI: retrievalURI, declaredKeywords: keywords, - declaredAnnotations: .init(values: JSONObject(admitted: annotations)), declaredConstraints: JSONObject(admitted: constraints), resourceURI: nil, resourcePointer: nil, referenceTargets: nil) + declaredAnnotations: .init(values: try JSONObject.indexed(admitted: annotations, meter: &meter)), declaredConstraints: try JSONObject.indexed(admitted: constraints, meter: &meter), resourceURI: nil, resourcePointer: nil, referenceTargets: nil) } } extension JSONSchemaDocument { diff --git a/Sources/JSONSchema/Output/JSONValidationOutputAdapter.swift b/Sources/JSONSchema/Output/JSONValidationOutputAdapter.swift index d2c7aea..4f0b6f8 100644 --- a/Sources/JSONSchema/Output/JSONValidationOutputAdapter.swift +++ b/Sources/JSONSchema/Output/JSONValidationOutputAdapter.swift @@ -46,12 +46,12 @@ private enum JSONValidationOutputAdapter { try meter.work(); try meter.require(.objectMembers,UInt64(members.count)+1) for _ in key.utf8 { try meter.work(); try meter.charge(.diagnosticBytes) } try JSONAdmission.text(key,key: true,meter: &meter,owned: true) - try meter.retain(JSONLogicalWeights.member+JSONLogicalWeights.indexSlot) + try meter.retain(JSONLogicalWeights.member) members.append(.init(try JSONKey(key,meter: &meter),value: value)) } static func object(_ members: [JSONMember],depth: UInt64,meter: inout JSONWorkMeter) throws -> JSON { try meter.require(.maxDepth,depth); try meter.charge(.nodes); try meter.retain(JSONLogicalWeights.node) - return .object(JSONObject(admitted: members)) + return .object(try JSONObject.indexed(admitted: members, meter: &meter)) } static func array(_ values: [JSON],meter: inout JSONWorkMeter) throws -> JSON { try meter.require(.maxDepth,2); try meter.charge(.nodes); try meter.retain(JSONLogicalWeights.node); return .array(values) diff --git a/Tests/JSONTests/Release1_1/IndexPublicationTests.swift b/Tests/JSONTests/Release1_1/IndexPublicationTests.swift new file mode 100644 index 0000000..c6f6f2a --- /dev/null +++ b/Tests/JSONTests/Release1_1/IndexPublicationTests.swift @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: Apache-2.0 +import Testing +@testable import JSON + +private struct WideFields: Encodable { + let keys: [String] + func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: JSONCodingKey.self) + for (position, key) in keys.enumerated() { try container.encode(position, forKey: JSONCodingKey(key)) } + // Replacing keeps the first key at its insertion position. + try container.encode(0, forKey: JSONCodingKey(keys[0])) + } +} + +@Test func indexedPublicationCoversBuildersParserNativeCodingAndEditing() throws { + let keys = (0..<129).map { "field-\($0)" } + ["\u{e9}", "e\u{301}", "", "/", "~"] + let members = keys.enumerated().map { JSONMember($0.element, value: .number(JSONNumber(integer: $0.offset))) } + let checked = try JSONObject(members: members) + let built = try JSONObject.build { builder in + for member in members { try builder.append(member.key.string, value: member.value) } + } + let parsed = try JSONParser().parse(JSONWriter().write(.object(checked))).objectValue! + let coded = try JSONValueEncoder().encode(WideFields(keys: keys)).objectValue! + let edited = try checked.setting(.boolean(true), for: "replacement").removing("replacement") + for object in [checked, built, parsed, coded, edited] { + #expect(object.members.map(\.key) == checked.members.map(\.key)) + for member in members { + #expect(object[member.key] == member.value) + #expect(try JSON.object(object).requiredMember(member.key.string) == member.value) + } + #expect(object["absent"] == nil) + } +} + +@Test func controlledIndexCollisionsAndRefusedRehashPreservePriorMetadata() throws { + var index = JSONExactKeyIndex(fingerprintMask: 0) + var meter = try JSONWorkMeter(limits: .offline, domain: .core) + for position in 0..<257 { + #expect(try index.insert(JSONKey("collision-\(position)"), position: position, meter: &meter)) + } + for position in 0..<257 { #expect(try index.find(JSONKey("collision-\(position)"), meter: &meter) == position) } + #expect(try !index.insert(JSONKey("collision-1"), position: 999, meter: &meter)) + #expect(try index.find(JSONKey("absent"), meter: &meter) == nil) + var bounded = JSONExactKeyIndex() + var limits = JSONLimits.balanced; limits[.retainedBytes] = 32 * 48 + var limited = try JSONWorkMeter(limits: limits, domain: .core) + for position in 0..<31 { #expect(try bounded.insert(JSONKey("key-\(position)"), position: position, meter: &limited)) } + #expect(throws: JSONFailure.self) { try bounded.insert(JSONKey("key-31"), position: 31, meter: &limited) } + for position in 0..<31 { #expect(bounded.find(JSONKey("key-\(position)")) == position) } + #expect(bounded.find(JSONKey("key-31")) == nil) +} + +@Test func indexedDuplicateRecognitionRetainsSecondKeyLocationAndNonthrowingFinish() throws { + let fields = (0..<64).map { "\"k\($0)\":null" }.joined(separator: ",") + let prefix = "{" + fields + "," + // The value/colon after the duplicate is malformed; duplicate still wins. + let malformed = prefix + "\"k0\" ???" + do { + _ = try JSONParser().parse(malformed) + Issue.record("Duplicate was not recognized") + } catch let error as JSONFailure { + #expect(error.code == .duplicateKey) + #expect(error.location?.byteOffset == UInt64(prefix.utf8.count)) + } + var builder = try JSONObjectAccumulator() + for position in 0..<64 { try builder.append("k\(position)", value: .null) } + #expect(throws: JSONFailure.self) { try builder.append("k0", value: .boolean(true)) } + let completed = builder.finish() + #expect(completed.count == 64) + #expect(completed["k0"] == .null) +} + +@Test func indexedGrowthAndCollisionScansCheckCancellation() async throws { + let task = Task.detached { () throws -> Bool in + var meter = try JSONWorkMeter(limits: .offline, domain: .core) + var index = JSONExactKeyIndex(fingerprintMask: 0) + for position in 0..<64 { _ = try index.insert(JSONKey("key-\(position)"), position: position, meter: &meter) } + let starting = meter.statistics()[.workUnits] + meter.checkpointObserver = { work in + if work > starting { withUnsafeCurrentTask { $0?.cancel() } } + } + do { + for position in 64..<1000 { _ = try index.insert(JSONKey("key-\(position)"), position: position, meter: &meter) } + return false + } catch is CancellationError { + // Prior keys remain valid after an interrupted owned search/growth. + return index.find(JSONKey("key-0")) == 0 && index.find(JSONKey("key-63")) == 63 + } + } + #expect(try await task.value) +} diff --git a/Tools/Release1_1Probes/ConstructionBenchmark.swift b/Tools/Release1_1Probes/ConstructionBenchmark.swift new file mode 100644 index 0000000..d6981e1 --- /dev/null +++ b/Tools/Release1_1Probes/ConstructionBenchmark.swift @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: Apache-2.0 +import Foundation +import Darwin +import CryptoKit +@testable import JSON + +private final class AllocationObservations { + var snapshots: [[String: UInt64]] = [] + func capture(_ position: Int) { + var info = malloc_statistics_t(); malloc_zone_statistics(nil, &info) + snapshots.append(["position": UInt64(position), "blocksInUse": UInt64(info.blocks_in_use), "bytesInUse": UInt64(info.size_in_use)]) + } +} +private struct Fields: Encodable { + let keys: [String] + var observations: AllocationObservations? = nil + func encode(to encoder: any Encoder) throws { + var c = encoder.container(keyedBy: JSONCodingKey.self) + observations?.capture(0) + for (position, key) in keys.enumerated() { + try c.encodeNil(forKey: JSONCodingKey(key)) + if [1,32,256,1024,keys.count].contains(position + 1) { observations?.capture(position + 1) } + } + } +} +private struct Sample: Encodable { + let operation: String, width: Int, shape: String, sample: Int, status: String + let inputSHA256: String, seconds: Double + let work: UInt64?, logicalPeak: UInt64?, failure: String? + let processPeakRSS: Int64, allocatorBlocksInUse: UInt64, allocatorBytesInUse: UInt64 +} +@main struct ConstructionBenchmark { + static func main() throws { + var samples: [Sample] = [], allocationRows: [[String: String]] = [] + for width in [0,1,4,8,16,32,64,256,1024,10000] { + for shape in ["ascii", "prefix", "unicode"] { + let keys = (0..= 0 || failure != nil { + samples.append(.init(operation: operation, width: width, shape: shape, sample: sample, + status: failure == nil ? "passed" : "refused", inputSHA256: digest, + seconds: Double(duration.seconds) + Double(duration.attoseconds)/1e18, + work: stats?[.workUnits], logicalPeak: stats?.peakLogicalRetainedBytes, failure: failure, + processPeakRSS: Int64(usage.ru_maxrss), allocatorBlocksInUse: UInt64(info.blocks_in_use), allocatorBytesInUse: UInt64(info.size_in_use))) + } + if failure != nil { break } + } + } + if [32,256,1024].contains(width) { + let observer = AllocationObservations() + _ = try JSONValueEncoder(options: .init(limits: .offline)).encode(Fields(keys: keys, observations: observer)) + let data = try JSONEncoder().encode(observer.snapshots) + allocationRows.append(["width": String(width), "shape": shape, "snapshots": String(decoding: data, as: UTF8.self)]) + } + } + } + let encoder = JSONEncoder(); encoder.outputFormatting = [.sortedKeys] + let url = URL(fileURLWithPath: CommandLine.arguments[1]) + try encoder.encode(samples).write(to: url) + try encoder.encode(allocationRows).write(to: url.deletingPathExtension().appendingPathExtension("allocations.json")) + print("CONSTRUCTION_BENCHMARK cells=120 samples=\(samples.count) warmups=3 measured_samples=9 resource_refusals=\(samples.filter { $0.status == "refused" }.count) allocation_cells=\(allocationRows.count)") + } +} diff --git a/Tools/Release1_1Probes/LookupBenchmark.swift b/Tools/Release1_1Probes/LookupBenchmark.swift index 457027c..5eeeaa4 100644 --- a/Tools/Release1_1Probes/LookupBenchmark.swift +++ b/Tools/Release1_1Probes/LookupBenchmark.swift @@ -21,7 +21,12 @@ private struct Sample: Encodable { } let digest = SHA256.hash(data: try JSONEncoder().encode(keys)).map { String(format: "%02x", $0) }.joined() // Caller-built fixture admission and member allocation are outside this lookup stage. + #if INDEX_METADATA + var fixtureMeter = try JSONWorkMeter(limits: .offline, domain: .core) + let object = try JSONObject.indexed(admitted: keys.map { JSONMember($0, value: .string($0)) }, meter: &fixtureMeter) + #else let object = JSONObject(admitted: keys.map { JSONMember($0, value: .string($0)) }) + #endif var queries = width == 0 ? [("miss", "absent")] : [("first", keys[0]), ("middle", keys[width/2]), ("last", keys[width-1]), ("miss", shape == "prefix" ? String(repeating: "p", count: 128) + "absent" : "absent")] if width > 8 { queries.append(("first-filtered", keys[8])) } for (name, key) in queries { From f3bb1118cf9731e3aec2ef9e4ed98f4c46796f25 Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Mon, 12 Oct 2026 10:06:01 +0200 Subject: [PATCH 4/6] Qualify wide index consumers concurrency and measured retention tradeoffs --- .../Release1.1/IndexMeasurements.md | 52 +++++++++++++++++++ Scripts/ci.py | 4 +- Tests/JSONPatchTests/IndexedObjectTests.swift | 24 +++++++++ .../JSONSchemaTests/IndexedObjectTests.swift | 16 ++++++ .../Release1_1/IndexPublicationTests.swift | 15 ++++++ 5 files changed, 109 insertions(+), 2 deletions(-) create mode 100644 Documentation/Architecture/Release1.1/IndexMeasurements.md create mode 100644 Tests/JSONPatchTests/IndexedObjectTests.swift create mode 100644 Tests/JSONSchemaTests/IndexedObjectTests.swift diff --git a/Documentation/Architecture/Release1.1/IndexMeasurements.md b/Documentation/Architecture/Release1.1/IndexMeasurements.md new file mode 100644 index 0000000..ebbceb1 --- /dev/null +++ b/Documentation/Architecture/Release1.1/IndexMeasurements.md @@ -0,0 +1,52 @@ +# Production index stage measurements + +The matched predecessor core is `5d01f88` (mobile branch `bf0ff41` changes only +resource packaging/platform documentation). The metadata/keyed-storage core is +`bf37941`. Swift 6.4, SDK 27.2, macOS 27.0, M1 Max/32 GiB, release optimization, +finite offline profile, three warmups and nine measured samples apply to both. +Exact benchmark source and static-library digests accompany the private native +receipts. Documentation/test-only successors reuse these bytes explicitly. + +Construction measures 120 cells: four paths, ten widths 0–10000, three key shapes. +The predecessor has 1044 successful samples and four explicit long-prefix +width-10000 work refusals. The candidate has 1080 successful samples. Never use a +refusal's elapsed time as successful throughput. Accumulator timing is available; +its public facade has no accounting report, so its work/retention is unavailable. + +| Stage / ASCII width | Before median ms | After median ms | Change | Before / after logical peak bytes | +| --- | ---: | ---: | ---: | ---: | +| Checked 8 | 0.0060 | 0.0039 | -35.9% | 808 / 808 | +| Checked 32 | 0.0510 | 0.0155 | -69.5% | 3136 / 5096 | +| Checked 1024 | 39.5464 | 0.4029 | -99.0% | 99360 / 132128 | +| Parse 1024 | 39.8769 | 1.1994 | -97.0% | 132160 / 197696 | +| Native keyed 1024 | 79.3653 | 1.4056 | -98.2% | 238624 / 336928 | +| Native keyed 10000 | 6930.3080 | 15.7802 | -99.8% | 2330032 / 4464688 | + +Lookup measures 129 query cells with 1161 samples per side, 20 lookups/sample. +The final serialized pair, after the overlapping compiler job completed, gives: + +| Query | Before / after median ms per 20 lookups | Change | Before / after work | +| --- | ---: | ---: | ---: | +| ASCII 8 last | 0.018042 / 0.018791 | +4.15% | 1620 / 1620 | +| ASCII 32 first filtered | 0.023833 / 0.022792 | -4.37% | 2020 / 2020 | +| ASCII 32 miss | 0.019375 / 0.007542 | -61.07% | 1400 / 580 | +| ASCII 1024 last | 0.526334 / 0.018541 | -96.48% | 42140 / 1540 | +| Prefix 10000 last | 6.195458 / 0.247250 | -96.01% | 426180 / 26580 | + +Retain and review every cell above the unchanged 10% trigger, including tiny +measurements near clock/call overhead. The disposition selects this candidate for +wide construction/lookup benefit with explicit additional spare-capacity retention. +It does not claim an improvement for every tiny case or a mobile crossover. +Construction/root admission/ordered rebuilding remain separate from lookup. + +Nine separately instrumented native-keyed cells record libmalloc live blocks/bytes +at selected field boundaries. Per-sample allocator observations and cumulative +`ru_maxrss` are process-wide; they are not one allocation per field or isolated +workload allocation counts. Logical weights remain Version 1 and are not RSS. + +First lookup samples were lost to a local receipt/output filename collision and +were rerun with distinct sample paths. An earlier retained pair overlapped compiler +work; the final serialized pair is used above. All actual failure/command records +remain evidence. These stage results precede the reader's shared-meter and ISO +correction; affected integrated performance must be refreshed on the final source. +The historical approximately 11% streaming disposition remains separate. diff --git a/Scripts/ci.py b/Scripts/ci.py index b44a6c9..d9e155c 100644 --- a/Scripts/ci.py +++ b/Scripts/ci.py @@ -6,7 +6,7 @@ from preflight import collect ROOT = pathlib.Path(__file__).resolve().parents[1] -SHARED_TESTS = 'independentCodecFacadesAndExactValuesCanBeSharedAcrossTasks|concurrentSchemaEvaluationsHaveExclusiveDynamicRegexAndAnnotationState|sharedRegexProgramHasExclusiveConcurrentSearchState' +SHARED_TESTS = 'independentCodecFacadesAndExactValuesCanBeSharedAcrossTasks|concurrentSchemaEvaluationsHaveExclusiveDynamicRegexAndAnnotationState|sharedRegexProgramHasExclusiveConcurrentSearchState|immutableIndexedObjectCanBeSharedAcrossTasks' R0_TESTS = 'readerProbe|indexProbes|duplicateRecognitionPrecedesValueAndClosingSyntax' def snapshot(): @@ -68,7 +68,7 @@ def check_result(command, code, log): counts = [int(n) for n in re.findall(r'Test run with (\d+) tests? in[^\n]*passed', log)] passed = code == 0 if command[:2] == ['swift', 'test']: - expected = [7] if R0_TESTS in command else [3] if 'Tools/MCPReadiness' in command else [1] if 'Tools/Qualification' in command else [1, 1, 1] if 'thread' in command else [8, 11, 12, 37, 67] + expected = [7] if R0_TESTS in command else [3] if 'Tools/MCPReadiness' in command else [1] if 'Tools/Qualification' in command else [1, 1, 2] if 'thread' in command else [8, 12, 12, 38, 68] passed = passed and sorted(counts) == expected if 'Tools/Qualification' in command: probes = re.findall(r'^CAP_PROBE (\w+) (\S+)$', log, re.MULTILINE) diff --git a/Tests/JSONPatchTests/IndexedObjectTests.swift b/Tests/JSONPatchTests/IndexedObjectTests.swift new file mode 100644 index 0000000..4eed68d --- /dev/null +++ b/Tests/JSONPatchTests/IndexedObjectTests.swift @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 +import Testing +import JSON +import JSONPatch + +@Test func wideIndexedPatchStagesRebuildExactMetadataAndKeepOrder() throws { + let members = (0..<128).map { JSONMember("field-\($0)", value: .null) } + [JSONMember("é", value: 1), JSONMember("e\u{301}", value: 2)] + let value = JSON.object(try JSONObject(members: members)) + let patch = try JSONPatch(operations: [ + .replace(path: JSONPointer(parsing: "/field-127"), value: true), + .test(path: JSONPointer(parsing: "/e\u{301}"), value: 2), + .copy(from: JSONPointer(parsing: "/é"), path: JSONPointer(parsing: "/copied")) + ]) + guard case .document(let changed) = try patch.applying(to: value) else { Issue.record("Removed a present root"); return } + #expect(try changed.requiredMember("field-127") == true) + #expect(try changed.requiredMember("e\u{301}") == 2) + #expect(try changed.requiredMember("copied") == 1) + #expect(changed.objectValue!.members.map(\.key) == members.map(\.key) + [JSONKey("copied")]) + let merged = try JSONMergePatch(JSONParser().parse(#"{"field-127":null,"é":3}"#)).applying(to: changed) + #expect(try merged.presence(of: "field-127") == .absent) + #expect(try merged.requiredMember("é") == 3) + #expect(try merged.requiredMember("e\u{301}") == 2) + #expect(value.objectValue!.count == 130) +} diff --git a/Tests/JSONSchemaTests/IndexedObjectTests.swift b/Tests/JSONSchemaTests/IndexedObjectTests.swift new file mode 100644 index 0000000..984eb94 --- /dev/null +++ b/Tests/JSONSchemaTests/IndexedObjectTests.swift @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 +import Testing +import JSON +import JSONSchema + +@Test func wideIndexedInstancesPreserveRequiredDependenciesAndExactKeys() throws { + let members = (0..<128).map { JSONMember("field-\($0)", value: .null) } + [JSONMember("é", value: 1), JSONMember("e\u{301}", value: 2)] + let value = JSON.object(try JSONObject(members: members)) + let schema = try JSONParser().parse(#"{"type":"object","required":["field-127","é","e\u0301"],"properties":{"field-127":{"type":"null"},"é":{"const":1},"e\u0301":{"const":2}},"dependentRequired":{"field-126":["field-127"]}}"#) + let plan = try JSONSchemaCompiler().compile(JSONSchemaDocument(schema)) + #expect(try JSONSchemaValidator().validate(value, against: plan).isValid) + let missing = JSON.object(try value.objectValue!.removing("field-127")) + #expect(try !JSONSchemaValidator().validate(missing, against: plan).isValid) + let changed = JSON.object(try value.objectValue!.setting(2, for: "é")) + #expect(try !JSONSchemaValidator().validate(changed, against: plan).isValid) +} diff --git a/Tests/JSONTests/Release1_1/IndexPublicationTests.swift b/Tests/JSONTests/Release1_1/IndexPublicationTests.swift index c6f6f2a..70f1c2b 100644 --- a/Tests/JSONTests/Release1_1/IndexPublicationTests.swift +++ b/Tests/JSONTests/Release1_1/IndexPublicationTests.swift @@ -89,3 +89,18 @@ private struct WideFields: Encodable { } #expect(try await task.value) } + +@Test func immutableIndexedObjectCanBeSharedAcrossTasks() async throws { + let members = (0..<256).map { JSONMember("key-\($0)", value: .number(JSONNumber(integer: $0))) } + let value = JSON.object(try JSONObject(members: members)) + try await withThrowingTaskGroup(of: Void.self) { group in + for position in 0..<32 { + group.addTask { + for _ in 0..<10 { + let selected = try value.requiredMember("key-\(position * 7)"); #expect(selected == .number(JSONNumber(integer: position * 7))) + } + } + } + try await group.waitForAll() + } +} From 180b5c71eeb09d8d2e700296aa2bc772df970763 Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Mon, 12 Oct 2026 10:25:08 +0200 Subject: [PATCH 5/6] Correct exact discovered bundle cardinalities for the index lane --- Scripts/ci.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Scripts/ci.py b/Scripts/ci.py index d9e155c..3341fd0 100644 --- a/Scripts/ci.py +++ b/Scripts/ci.py @@ -68,7 +68,7 @@ def check_result(command, code, log): counts = [int(n) for n in re.findall(r'Test run with (\d+) tests? in[^\n]*passed', log)] passed = code == 0 if command[:2] == ['swift', 'test']: - expected = [7] if R0_TESTS in command else [3] if 'Tools/MCPReadiness' in command else [1] if 'Tools/Qualification' in command else [1, 1, 2] if 'thread' in command else [8, 12, 12, 38, 68] + expected = [7] if R0_TESTS in command else [3] if 'Tools/MCPReadiness' in command else [1] if 'Tools/Qualification' in command else [1, 1, 2] if 'thread' in command else [9, 11, 12, 38, 68] passed = passed and sorted(counts) == expected if 'Tools/Qualification' in command: probes = re.findall(r'^CAP_PROBE (\w+) (\S+)$', log, re.MULTILINE) From 74b09b7d0aabf9c21cad9242b3f49a139c91d0aa Mon Sep 17 00:00:00 2001 From: Thorsten Fuchs <76904008+ThorFuchs@users.noreply.github.com> Date: Mon, 12 Oct 2026 10:41:40 +0200 Subject: [PATCH 6/6] Record disposition for every index benchmark review trigger --- .../Release1.1/IndexMeasurements.md | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/Documentation/Architecture/Release1.1/IndexMeasurements.md b/Documentation/Architecture/Release1.1/IndexMeasurements.md index ebbceb1..e741757 100644 --- a/Documentation/Architecture/Release1.1/IndexMeasurements.md +++ b/Documentation/Architecture/Release1.1/IndexMeasurements.md @@ -50,3 +50,44 @@ work; the final serialized pair is used above. All actual failure/command record remain evidence. These stage results precede the reader's shared-meter and ISO correction; affected integrated performance must be refreshed on the final source. The historical approximately 11% streaming disposition remains separate. + +## Complete trigger review + +All 11 construction and 16 lookup cells above 10% are retained below. +Construction absolute increases are at most 1.541 microseconds per operation; +lookup increases are at most 1.625 microseconds per twenty queries. These are +observed regressions, without a statistical noise or causal attribution claim. +Accept the small-object metadata/ownership cost for the measured wide-object +benefit, retaining the scalar lookup path below 32. Empty measurements establish +no throughput claim. Four predecessor refusals remain excluded from timing ratios. +The threshold stays 10%; mobile crossovers remain unqualified. + +| Operation / shape / width / query | Before / after microseconds | Change | Disposition | +| --- | ---: | ---: | --- | +| accumulator / prefix / 0 / — | 0.584 / 0.666 | +14.04% | No throughput claim; retain observation | +| checked / prefix / 0 / — | 0.792 / 0.875 | +10.48% | No throughput claim; retain observation | +| checked / unicode / 0 / — | 0.709 / 0.833 | +17.49% | No throughput claim; retain observation | +| checked / ascii / 1 / — | 1.125 / 1.417 | +25.96% | Accept measured small-case tradeoff | +| native-keyed / unicode / 0 / — | 2.125 / 2.416 | +13.69% | No throughput claim; retain observation | +| native-keyed / ascii / 1 / — | 4.041 / 4.542 | +12.40% | Accept measured small-case tradeoff | +| native-keyed / unicode / 1 / — | 3.667 / 4.042 | +10.23% | Accept measured small-case tradeoff | +| parse / ascii / 1 / — | 2.625 / 2.958 | +12.69% | Accept measured small-case tradeoff | +| parse / prefix / 1 / — | 11.792 / 13.333 | +13.07% | Accept measured small-case tradeoff | +| parse / unicode / 1 / — | 2.542 / 2.834 | +11.49% | Accept measured small-case tradeoff | +| parse / unicode / 4 / — | 6.500 / 7.500 | +15.38% | Accept measured small-case tradeoff | +| lookup / ascii / 0 / miss | 0.083 / 0.167 | +101.20% | No throughput claim; retain observation | +| lookup / prefix / 0 / miss | 0.042 / 0.167 | +297.62% | No throughput claim; retain observation | +| lookup / unicode / 0 / miss | 0.042 / 0.166 | +295.24% | No throughput claim; retain observation | +| lookup / ascii / 1 / miss | 0.625 / 0.875 | +40.00% | Accept measured small-case tradeoff | +| lookup / unicode / 1 / first | 3.542 / 3.916 | +10.56% | Accept measured small-case tradeoff | +| lookup / unicode / 1 / last | 3.542 / 3.916 | +10.56% | Accept measured small-case tradeoff | +| lookup / unicode / 1 / middle | 3.542 / 3.916 | +10.56% | Accept measured small-case tradeoff | +| lookup / unicode / 1 / miss | 0.583 / 0.875 | +50.09% | Accept measured small-case tradeoff | +| lookup / ascii / 4 / first | 2.875 / 3.208 | +11.58% | Accept measured small-case tradeoff | +| lookup / ascii / 4 / miss | 2.334 / 2.834 | +21.42% | Accept measured small-case tradeoff | +| lookup / unicode / 4 / miss | 2.333 / 2.833 | +21.43% | Accept measured small-case tradeoff | +| lookup / ascii / 8 / miss | 4.542 / 5.292 | +16.51% | Accept measured small-case tradeoff | +| lookup / unicode / 8 / miss | 4.542 / 5.292 | +16.51% | Accept measured small-case tradeoff | +| lookup / ascii / 16 / first | 2.916 / 3.208 | +10.01% | Accept measured small-case tradeoff | +| lookup / ascii / 16 / miss | 8.625 / 10.250 | +18.84% | Accept measured small-case tradeoff | +| lookup / unicode / 16 / miss | 8.916 / 9.959 | +11.70% | Accept measured small-case tradeoff |